In de vorige posting liet ik je zien hoe je een vierkant definieert. Ik sloot af met de opmerking dat als je een kubus wilt tekenen, je vooral je gang moest gaan maar ik zou de code niet voor je intypen. De reden daarvoor is dat het heel veel saai werk is (waar heel makkelijk fouten insluipen!). Nou is het altijd aan te raden om mensen zelf aan het werk te zetten als ze iets willen leren, daar leren ze immers het meest van. Toch vond ik het een beetje flauw van me. Vandaar dat ik de code hier alsnog plaats. Let op: hier komt 'ie:
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D
Positions="0 0 0, 1 0 0, 1 0 -1, 0 0 -1,
0 1 0, 1 1 0, 1 1 -1, 0 1 -1"
TriangleIndices="0 1 5, 0 5 4,
1 2 6, 1 6 5,
2 3 7, 2 7 6,
3 0 4, 3 4 7,
4 5 6, 4 6 7,
0 3 2, 0 2 1"
/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Blue"/>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush="HotPink"/>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
Tja. Zoals ik al zei: niet echt spannend, maar wel veel werk en vooral saai. Ik definieer eerst de punten voor het grondvlak, daarna de punten voor het bovenste vlak. Dan in de TriangleIndices verbind ik alle punten met elkaar, tegen de klok in. Als je nu met je camera gaat draaien zal er een box uitkomen. Maar... hij ziet er een beetje raar uit:
(ik gebruik voor de camera als Position (2.1, 2.0, 2.0) en als LookDirection (-0.5, -0.7, -1.0) )
Geen mooie vlakken, maar wel een mooi verloop in de kleuren. Niet helemaal zoals mijn box er in deel 3 er uit zag. Hoe kan dat nou?
Het antwoord heeft te maken met de belichting en hoe WPF die berekent. In deel 3 vertelde ik je dat de DirectionalLight voor ieder punt berekend hoeveel licht er op valt. Dat doet hij door naar de vector van de DirectionalLight.Direction te kijken en dan te berekenen hoe die zich verhoudt tot de normalvectors van onze vlakken. Als de direction vector het tegenovergestelde is van de normalvector van ons vlak, is de lichtinval 100%. Als hij er haaks op staat (of zelfs enigzins de zelfde richting opgaat) dan is de lichtinval 0%. En voor vectoren daar tussen in is de lichtinval een percentage van de totale hoeveelheid licht.
Dit doet WPF echter niet voor iedere normalvector per driehoek maar voor iedere normalvector op een punt.... Dus: WPF berekent voor ieder punt in onze objecten een normal vector en vermenigvuldigt die met de direction vector van het licht. Maar hoe berekent hij nu de normalvector van een punt? Een punt is namelijk geen vlak (daar heb je minstens 3 punten voor nodig) dus hoe doet hij dat nu? Het antwoord is simpel. WPF kijkt naar de vlakken waar dit punt deel uit van maakt, berekent voor die vlakken de normalvector en dat is dat de normalvector van de punten die bij dat vlak horen.
In theorie is dat simpel, in de praktijk is dat wat lastiger. Kijk bijvoorbeeld eens naar ons punt met de coordinaten (0,0,0). Bij welk vlak hoort dat? Volgens mij hoort dat bij 3 vlakken: het voorste vlak, het linkervlak en het onderste vlak. Dus hoe ziet de normalvector er dan uit? Tja: dat moet er wel uitzien als in het volgende plaatje. Ik heb gelijk de vectoren voor de andere punten erbij geplaatst.
Als de lichtinval op het grondvlak 100% zou moeten zijn, zou dat alleen gebeuren als de normaalvectoren loodrecht op dat vlak staan. Maar aangezien de verschillende hoekpunten onderdeel van meerdere vlakken zijn, wordt de normaalvectoren van die punten het gemiddelde van de normaalvectoren van de drie vlakken waar ieder punt bij hoort.
Erger nog: de kleur tussen de punten A en E wordt voor iedere pixel tussen die punten berekend: aangezien A en E totaal verschillende normaalvectoren en dus lichtintensiteiten hebben, zal het resultaat een verloop zijn. WPF gaat de kleuren mooi in elkaar over laten lopen.
Dit effect kan heel handig zijn. Als je een gebogen oppervlakte wilt tekenen zonder al te veel driehoeken te moeten tekenen, is dit precies het effect dat je wilt: door de gradients lijken de oppervlaktes gebogen te zijn. Maar in het geval van een box is dat helemaal niet handig!
We kunnen dit oplossen door het probleem bij de wortel aan te pakken. Als de oorzaak van het probleem ligt bij het delen van punten door verschillende vlakken, moeten we eenvoudigweg voor ieder vlak een eigen set punten definieren.
We krijgen dus in plaats van 8 punten 6 vlakken maal 4 punten per vlak = 24 punten (we delen per vierkant wel een paar punten maar aangezien die in hetzelfde vlak liggen geeft het niet: ze hebben toch al dezelfde normaalvector). Dat er van die 24 punten veel hetzelfde zijn is iets dat we dan maar voor lief moeten nemen.
De code ziet er nu als volgt uit:
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D
Positions="0 0 0, 1 0 0, 1 1 0, 0 1 0,
1 0 0, 1 0 -1, 1 1 -1, 1 1 0,
1 0 -1, 0 0 -1, 0 1 -1, 1 1 -1,
0 0 -1, 0 0 0, 0 1 0, 0 1 -1,
0 1 0, 1 1 0, 1 1 -1, 0 1 -1,
0 0 0, 0 0 -1, 1 0 -1, 1 0 0"
TriangleIndices="0 1 2 0 2 3
4 5 6 4 6 7
8 9 10 8 10 11
12 13 14 12 14 15
16 17 18 16 18 19
20 21 22 20 22 23"
/>
</GeometryModel3D.Geometry>
Meer punten maar maak je daar geen zorgen om: WPF is erg efficient met het geheugengebruik en dit zal geen noemenswaardige effecten op je performance hebben.
Het resultaat is een stuk beter:
Dit lijkt tenminste op een kubus!
Maar nu willen we niet 1 kubus hebben, maar een paar honderd. En allemaal op verschillende plekken, en uiteraard van verschillende afmetingen! Dat wordt een hoop rekenwerk ben ik bang. Of toch niet? Kunnen we dit niet op de een of andere manier in code vatten?
Ik zou die vraag niet stellen als het antwoord niet "ja" zou zijn. En dat is precies wat ik in de volgende post ga doen! We maken een C# class die dit voor ons doet zodat we in XAML de volgende code kunnen plakken:
<detrio:Box LeftBottomCorner="0 0 0"
Width="2.0"
Height="1.0"
Depth="1.5"
>
<detrio:Box.Material>
<DiffuseMaterial Brush="Blue"/>
</detrio:Box.Material>
</detrio:Box>
En dat is een stuk beter leesbaar dan dat gedoe met die Positions toch?
PS We hadden het probleem ook anders op kunnen lossen: we hadden zelf de normals kunnen berekenen en dan toch de punten kunnen hergebruiken. Ik wilde je echter laten zien hoe hergebruik van punten rare effecten op kan leveren. Daarnaast is het berekenen van Normals vrij ingewikkeld als je verder gaat dan een simpel kubusje, waardoor het veel eenvoudiger is om de punten te herdefinieren. Het zelf berekenen van normals kan wel een performance winst opleveren...