Välkommen! Detta är tredje delen av vår serie på 3D grafikmotorer. Om du gjorde det här långt in i serien, kommer du vara glad att veta att den här delen kommer att bli mycket ljusare på matematisk aspekt av 3D-motorer, och istället fokuserar på mer praktiska saker - i synnerhet att lägga till en kamera och ett grundläggande reningssystem.
Tips: Om du inte har läst de två första delarna ännu, föreslår jag starkt att du gör innan du fortsätter.
Du kan också få extra hjälp på Envato Studio, där du kan välja mellan ett brett sortiment av 3D-design och modellering av hög kvalitet från erfarna leverantörer.
3D Design & Modeling tjänster på Envato StudioFörst av, låt oss ta en titt på de klasser som vi har skapat hittills:
Point Class Variables: num tuple [3]; // (x, y, z) Operatörer: Peka AddVectorToPoint (Vector); Punkt SubtraherVectorFromPoint (Vector); Vector SubtractPointFromPoint (Punkt); Null SetPointToPoint (Point); // flytta punkt till specificerad punkt Funktioner: drawPoint; // rita en punkt i sin position tuple Vector Class Variables: num tuple [3]; // (x, y, z) Operatörer: Vector AddVectorToVector (Vector); Vector SubtraherVectorFromVector (Vector); Vector RotateXY (grader); Vector RotateYZ (grader); Vector RotateXZ (grader); Vektorskala (s0, s1, s2); // parametrar: skalering längs varje axel
Att använda dessa två klasser på egen hand har visat sig lite rörigt hittills, och att dra alla möjliga punkter kan dränera systemets minne ganska snabbt. För att lösa dessa problem ska vi introducera en ny klass i vår spelmotor: The kamera.
Vår kamera kommer att vara där all vår återgivning sker exklusivt; den kommer att gallra alla våra objekt till skärmen, och det kommer också att hantera en lista över alla våra poäng.
Men innan vi kan komma till allt detta måste vi först prata lite om utslagning.
Culling är per definition valet av objekt från en större grupp av objekt. För vår spelmotor är det lilla urvalet vi tar de punkter som vi vill dra till skärmen. Den större gruppen av objekt kommer att vara varje punkt som existerar.
Om du gör detta minskar drastiskt din motors dränering på ett systems minne, genom att bara rita vad en spelare faktiskt kan se, snarare än en hel världs värden av poäng. I vår motor ska vi göra detta genom att ställa in parametrar för a visa utrymme.
Vårt visningsutrymme definieras över alla tre av de traditionella axlarna: x, y och z. Dess x-definition kommer att bestå av allt mellan fönstrets vänstra och högra gränser, dess y-definition kommer att bestå av allt mellan fönstrets övre och nedre gräns och dess z-definition kommer att ligga mellan 0
(där kameran är inställd) och vår spelares synvinkel (för vår demonstration använder vi ett godtyckligt värde av 100
).
Innan vi ritar en punkt kommer vår kameraklass att kontrollera om den punkten ligger inom vårt visningsutrymme. Om det gör så kommer poängen att dras; annars kommer det inte.
Med den grundläggande förståelsen för culling kan vi härleda att vår klass kommer att se ut så här, hittills:
Kameraklass Vars: int minX, maxX; // minsta och maximala gränser för X int minY, maxY; // minsta och maximala gränser för Y int minZ, maxZ; // minsta och maximala gränsen för Z
Vi kommer också att få vår kamera att hantera all rendering för vår motor också. Beroende på motorn kommer du att upptäcka att renderare ofta skiljs från kamerans system. Detta görs vanligen för att hålla systemen inkapslade snyggt, eftersom de två kan bli ganska röriga om de hålls ihop, beroende på omfattningen av din motor. För våra syften är det dock enklare att behandla dem som en.
Först ska vi vilja ha en funktion som kan kallas externt från klassen som kommer att dra scenen. Denna funktion kommer att cykla genom var och en av de punkter som existerar, jämföra dem med kamerans uttagsparametrar och rita dem om så är tillämpligt.
Tips: Om du vill separera ditt kamerasystem från din renderer kan du helt enkelt skapa en Renderer
klass, ta kamerans system ihop punkterna, lagra dem som ska dras i en array och skicka sedan den här matrisen till dra()
funktionen av din renderer.
Den slutliga delen av vår kameraklass kommer att bli dess poänghanteringssystem. Beroende på vilket programmeringsspråk du använder kan det här bara vara en enkel uppsättning av alla objekt som kan dras (vi hanterar mer än bara poäng i senare delar). Alternativt kan du behöva använda språkets standardobjekt föräldraklass. Om du är otur olycklig måste du skapa ditt eget föremål föräldraklass och ha varje dragbar klass (hittills endast poäng) vara ett barn i den klassen.
Efter att ha lagt till det i klassen skulle en grundläggande översikt över vår kamera se ut så här:
Kameraklass Vars: int minX, maxX; // minsta och maximala gränser för X int minY, maxY; // minsta och maximala gränser för Y int minZ, maxZ; // minsta och maximala gränser för Z-arrayobjektenInWorld; // en grupp av alla existerande objekt Funktioner: null drawScene (); // ritar alla nödvändiga objekt till skärmen, returnerar ingenting]
Med dessa tillägg, låt oss förbättra lite på det program som vi gjorde förra gången.
Vi ska skapa ett enkelt punktritningsprogram, med det provprogram som vi skapade förra gången som utgångspunkt.
I denna iteration av programmet ska vi lägga till användningen av vår nya kameraklass. När D tangenten trycks in, kommer programmet att omradera skärmen utan att kasta ut, vilket visar antalet objekt som gjordes i det övre högra hörnet av skärmen. När C tangenten trycks in, kommer programmet att redrawera skärmen med culling, vilket också visar antalet återställda objekt.
Låt oss ta en titt på koden:
main // setup för ditt favoritgrafik API här // inställning för tangentbordsinmatning (kanske inte krävs) här var kamera = ny kamera (); // skapa en instans av kameraklassen camera.objectsInWorld [100]; // skapa 100 objektutrymmen i kamerans array // sätt kamerans bildrymd camera.minX = 0; camera.maxX = screenWidth; camera.minY = 0; camera.maxY = screenHeight; camera.minZ = 0; camera.maxZ = 100; för (int x = 0; x < camera.objectsInWorld.length; x++) //Set its location to a random point on the screen camera.objectsInWorld[x].tuple = [random(-200,1000), random(-200,1000), random(-100,200)); function redrawScreenWithoutCulling() //this function clears the screen and then draws all of the points ClearTheScreen(); //use your Graphics API's clear screen function for(int x = 0; x < camera.objectsInWorld.length; x++) camera.objectsInWorld[x].drawPoint(); //draw the current point to the screen while(esc != pressed) // the main loop if(key('d') == pressed) redrawScreenWithoutCulling(); if(key('c') == pressed) camera.drawScene(); if(key('a') == pressed) Point origin = new Point(0,0,0); Vector tempVector; for(int x = 0; x < camera.objectsInWorld.length; x++) //store the current vector address for the point, and set the point tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added camera.objectsInWorld[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location camera.objectsInWorld[x].addVectorToPoint(tempVector.scale(0.5,0.5,0.5)); if(key('s') == pressed) Point origin = new Point(0,0,0); //create the space's origin as a point Vector tempVector; for(int x = 0; x < camera.objectsInWorld.length; x++) //store the current vector address for the point, and set the point tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added camera.objectsInWorld[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location camera.objectsInWorld[x].addVectorToPoint(tempVector.scale(2.0,2.0,2.0)); if(key('r') == pressed) Point origin = new Point(0,0,0); //create the space's origin as a point Vector tempVector; for(int x = 0; x < camera.objectsInWorld.length; x++) //store the current vector address for the point, and set the point tempVector = camera.objectsInWorld[x].subtractPointFromPoint(origin); //reset the point so that the scaled vector can be added camera.objectsInWorld[x].setPointToPoint(origin); //scale the vector and set the point to its new, scaled location camera.objectsInWorld[x].addVectorToPoint(tempVector.rotateXY(15));
Nu kan du se, första hand, kraften att kasta! Observera att om du tittar igenom provkoden görs några saker lite annorlunda för att göra demo mer webbvänliga. (Du kan kolla in min enkla demo här.)
Med ett kamera och reningssystem under ditt bälte kan du tekniskt säga att du har skapat en 3D-spelmotor! Det kanske inte är alltför imponerande än, men det är på väg.
I vår nästa artikel tittar vi på att lägga till några geometriska former i vår motor (nämligen linjesegment och cirklar) och vi talar om de algoritmer som kan användas för att passa deras ekvationer till bildpunkterna på en skärm.