dotNed

Welkom bij dotNed Inloggen | Aanmelden | Help
in Zoeken

Dennis' avonturen in .net

3D Graphics in WPF, deel 5 (GeometryModel3D)

Eindelijk: we gaan dingen tekenen! Na alle noodzakelijk gepraat over camera's, lampen, punten en vectoren gaan we eindelijk aan de slag met iets dat zichtbaar op het scherm komt!

In het eerste voorbeeld gaf ik al een vierkantje weer, zodat we in ieder geval iets op het scherm zagen. Ik geef toe: echt indrukwekkend is het niet maar het was een begin!

Als je denkt: waarom liet hij de code voor een vierkantje zien en in de screenshots liet hij kubusjes zien, dan is het antwoord: omdat dat eenvoudiger was.

In WPF is er helaas geen enkele class met de naam Cube. Of Cylinder. Of Sphere, Torus, Teapot of wat voor fraais dan ook. WPF kent alleen maar verzamelingen van punten. Een vierkant is voor WPF niets anders dan een set met 4 punten die op een bepaalde plek in de 3D scene staan en de volgorde waarin die punten getekend moeten worden. Nu is een vierkant niet zo ingewikkeld: het zijn maar 4 punten verbonden door 4 lijnen. Een kubus is al ingewikkelder, die bestaat immers uit 8 punten verbonden door 12 lijnen.

Het wordt nog ingewikkelder als je je realiseert dat WPF eigenlijk alleen maar driehoeken kan tekenen.... Een vierkant bestaat uit 2 driehoeken, een kubus uit 12 driehoeken. Uit hoeveel driehoeken een bol bestaat ligt er maar net aan hoe je hem definieert, maar het aantal driehoeken loopt al gauw in de honderden of zelfs duizenden.

Nu is dat niets om je zorgen over te maken: grafische kaarten tegenwoordig zijn helemaal geoptimaliseerd in het tekenen van heel, heel erg veel driehoeken. Iets anders kennen ze niet maar driehoeken: daar zijn ze dol op!

imageLaten we even kijken naar ons vierkantje. Deze bestaat uit 4 punten (A, B, C, en D) en 2 driehoeken ( (A,B,C) en (A,C,D)). We vertellen WPF dus dat we een driehoek willen tekenen. We geven eerst de punten mee (de volgorde maakt eigenlijk niet uit). In ons geval dus de punten A, B en C. Dan vertellen we WPF dat hij de punten in een bepaalde volgorde moet verbinden. Vanuit punt A naar punt B, vanuit B naar punt C. Aangezien we met driehoeken werken en een driehoek altijd 3 zijdes heeft, 'weet' WPF dat het laaste punt dus vanuit C naar A gaat: hij sluit hem zelf wel af.

De volgorde van de lijnen is  wel belangrijk: dit moet altijd tegen de klok in! Waarom is dat nou weer? Een vlak (zoals onze driehoek) heeft twee kanten: een voorkant en een achterkant. Wat precies de voorkant is en wat de achterkant, weet WPF niet: dat hangt af van de plaats, de rotatie en van de plek van de camera. Om te bepalen wat nou de voorkant is berekent WPF de normaal vector uit. De drie opgeven punten A, B en C liggen in 1 vlak (in ons geval liggen ze allemaal op het vlak dat recht voor ons is: langs de Y as recht omhoog of omlaag). De normaal vector is een vector die daar haaks op staat.  Als we ons vlak zouden neerleggen zie je dat de normaalvector haaks op onze driehoek staat en uit het scherm wijst:image De voorkant van ons driehoekje is het punt vanuit waar de normaal vector wijst. WPF berekent dat door de 3 punten te nemen en daar een vector haaks op te zetten. Nu is de volgorde van belang. Omdat we aangegeven hebben dat we vanuit A naar B, vanuit B naar C (en automatisch vanuit C naar A) gaan, is de normaal vector een Vector3D (0,0,1). Als we nu de punten aangegeven hadden als vanuit A naar C, vanuit C naar B en vanuit B naar A (dus met de klok mee) dan had de normaal vector precies de andere kant opgegaan (0,0,-1). Dat betekent dus dat de achterkant van het driehoekje naar de kijker toe had gestaan!

Geloof me: deze fout ga je nog regelmatig maken.... Gelukkig is er een hulpmiddel die je hier bij helpt, daar kom ik later nog op.

Ok. We hebben dus drie punten gegeven en verteld in welke volgorde die aan elkaar geknoopt moeten worden. Maar nu zijn we er nog niet: dit is pas de helft van ons vierkantje! We hebben nog een driehoek te tekenen. Nu kunnen we aan WPF de punten A, C en D geven en vertellen dat ze in die volgorde aan elkaar moeten worden geknoopt. Als we dat gedaan hebben zijn we in principe klaar!

Laten we eens wat code bekijken. Onze vierkantje staat, zoals alles in 3D WPF, in een ModelVisual3D (of liever: in de Content van een ModelVisual3D). We voegen dus deze code toe aan onze WPF applicatie. Het resultaat (inclusief lampen en camera) is dan:

<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>
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <!-- Hier komt ons vierkantje en later onze kubus -->
            </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="0 0 5" 
                           LookDirection="0 0 -1" 
                           UpDirection="0 1 0" 
                           FieldOfView="60.0" 
                           />
        </Viewport3D.Camera>
    </Viewport3D>
</Window>

Deze code compileert niet: de content mag niet leeg zijn. Dit gaan we zo oplossen.

Ik ga vanaf nu niet meer de hele code plakken (ik word niet per blogregel betaald :-) ) maar ik hou me alleen bezig met het stuk dat in de plaats van de commentaar regel <!-- hier komt ... --> komt. Dat houdt het allemaal wat leesbaarder.

We kunnen behalve lampjes maar 1 ding toevoegen op deze plaats: een GeometryModel3D. Deze class bevat alles wat WPF nodig heeft om te tekenen (in een volgende versie van WPF hoop ik dat er naast de GeometryModel3D ook een Cube3D, Cylinder3D enzovoorts komen...) Deze GeometryModel3D heeft 4 belangrijke dependency  properties: Geometry, Material, BackMaterial en Transform.

In Geometry plaatsen we de punten en de verbindingen tussen de punten. In Material en BackMaterial geven we aan hoe de driehoeken er uit moeten gaan zien en in Transform kunnen we de boel draaien, vergroten, verkleinen en/of verplaatsen.

In Geometry kan maar 1 class geplaatst worden: de MeshGeometry3D. Een MeshGeometry3D is een verzameling van driehoeken.... Deze heeft (onder andere) de volgende properties: Positions en TriangleIndices. De Positions is een collection van Point3D structs, dus alle punten van onze driehoek, terwijl de TriangleIndices een collection van Int32 waardes zijn die aangeven in welke volgorde de punten verbonden moeten worden. Meer hebben we niet nodig!

De code ziet er als volgt uit:

<GeometryModel3D>
    <GeometryModel3D.Geometry>
        <MeshGeometry3D Positions="0 0 0, 1 0 0, 1 1 0"
                        TriangleIndices="0 1 2"
            />
    </GeometryModel3D.Geometry>
</GeometryModel3D>

Dit is de definitie van onze eerste driehoek. We geven de positions op in 3 Point3D structs, te weten (0 0 0), (1 0 0) en (1 1 0) oftewel punt A, B en C in mijn eerste tekening. Daarna geven we in TriangleIndices aan in welke volgorde ze moeten worden verbonden: begin bij punt 0 (0 0 0), dan naar punt 1 (1 0 0) en als laatste naar punt 2 (1 1 0) in het lijst met positions. Van punt 2 naar punt 0 hoeven we niet aan te geven, zoals ik al eerder zei doet WPF dat zelf wel.

En de andere driehoek? Die kunnen we als volgt doen:

<GeometryModel3D.Geometry>
    <MeshGeometry3D Positions="0 0 0, 1 0 0, 1 1 0, 0 0 0, 1 1 0, 0 1 0"
    TriangleIndices="0 1 2 3 4 5"
                    />
</GeometryModel3D.Geometry>

We definieren nu de punten A (0 0 0), C (1 1 0) en D (0 1 0) en de lijnen vanuit het 3e punt (dus de tweede keer dat we punt A definieren) naar het 4e punt (dus de tweede keer dat we punt C definieren) naar het 5e punt (dat is punt D). Mmmm.. dubbel definieren van punten? Niet handig! Dat vinden ze bij het WPF team ook, dus we hoeven de punten maar een keer te definieren en dan de TriangleIndices aan te passen:

<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>

Dat ziet er al veel beter uit. We definieren in Positions de punten A, B, C en D en vervolgens geven we in de TriangleIndices aan dat we vanuit 0 naar 1, vanuit 1 naar 2 (en dan automatisch vanuit 2 naar 0 gaan!). Dan zeggen we vanuit 0 naar 2 en vanuit 2 naar 3 (en automatisch vanuit 3 naar 0) en daarmee is ons vierkant genoeg gedefinieerd!

Overigens is WPF heel erg makkelijk als het gaat om het formatten van dit soort constructies. De volgende code is ook goed en een stuk leesbaarder:

<GeometryModel3D.Geometry>
    <MeshGeometry3D 
        Positions= "0 0 0, 
                    1 0 0, 
                    1 1 0, 
                    0 1 0"
        TriangleIndices="0 1 2 
                         0 2 3"
    />

Het is een kwestie van smaak maar ik denk dat deze versie beter is: je ziet beter waar de definitie van 1 punt ophoudt en de volgende begint (namelijk op de volgende regel), ook de driehoeken zijn duidelijker :van 0 naar 1 naar 2, vervolgens een nieuwe driehoek van 0 naar 2 naar 3. Ook het plaatsen van de komma's om de elementen te scheiden kan heel anders, maar ik vind dit een mooie layout.

Als je je code nu gaat draaien, zie je.... niets. Waarom niet? Omdat we geen Material hebt gedefinieerd. Over Materials wil ik het later uitgebreid hebben, maar om je toch iets te geven om mee te spelen even kort dit:

Een material kun je zien als het behangtje dat je op je objecten plak om ze zichtbaar te maken. GeometryModel3D heeft, zoals je al zag, een Material en een BackMaterial property. Deze gaan we nu zetten:

                <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>
                        <DiffuseMaterial Brush="Blue"/>
                    </GeometryModel3D.Material>
                </GeometryModel3D>

We hebben een blauw materiaal gemaakt zodat er in ieder geval iets zichtbaar is.

Oh ja; ik had het over het hulpmiddel om je te helpen voorkomen dat je de TriangleIndices in de verkeerde volgorde zet (zodat je tegen de achterkant aankijkt): ik definieer als ik in XAML objecten maak altijd de BackMaterial. Dit is het material dat aan de achterkant van onze object komt. Ik geef deze altijd een mooi opvallend kleurtje die ik niet gauw zou gebruiken in mijn applicatie. Op die manier valt een fout direct op. In de volgende code heb ik bewust de tweede driehoek verkeerd getekend maar aangezien ik ook een BackMaterial heb ingesteld is het meteen zichtbaar:

<GeometryModel3D>
    <GeometryModel3D.Geometry>
        <MeshGeometry3D 
            Positions= "0 0 0, 
                        1 0 0, 
                        1 1 0, 
                        0 1 0"
            TriangleIndices="0 3 2 
                             0 2 1" 
        />
    </GeometryModel3D.Geometry>
    <GeometryModel3D.Material>
        <DiffuseMaterial Brush="Blue"/>
    </GeometryModel3D.Material>
    <GeometryModel3D.BackMaterial>
        <DiffuseMaterial Brush="HotPink"/> 
    </GeometryModel3D.BackMaterial>
</GeometryModel3D>

Dat levert dit mooie plaatje op:

image

Ik heb dus TriangleIndices hier even met de klok meegeven om het effect weer te geven. Zet ze weer terug in de juiste plek om een mooi blauw vierkantje weer te geven.

Goed. De kubus.

Tja, die is niet echt ingewikkeld: een kubus bestaat uit 8 Point3D structures, 12 driehoeken en dus 12 TriangleIndices entries. Het is alleen een hoop werk om in te typen: ik laat het graag aan je zelf over om dat te doen :-)

Overigens: mocht je die kubus gaan maken: let er dan op dat je in ons voorbeeld recht van voren naar de kubus kijkt en dus de zijkanten en onder- en bovenkant niet ziet. Je zult met de camera positie en LookDirection moeten spelen om hem helemaal zichtbaar te maken!

Published Wednesday, August 27, 2008 2:49 PM door dvroegop
Filed Under: ,

Comments

No Comments
Anonymous comments are disabled

About dvroegop

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