26
Tags:
c++
Skrevet af
Bruger #1474
@ 11.08.2008
Render klasseDenne klasse vil indeholde hele vores scene som bliver skabt i klassens konstruktion. Derudover indeholder klassen nogle renderings funktioner. Den mest fundamentale er funktionen: RenderImage. Denne funktion har tre parameter som definer bredden og højden på den færdig renderet billede samt billedefilens navn. Funktionen vil starte renderings forløbet eller renderings pipeline. Lad os tage et nærmere kig på dette renderings forløb.
Først vil vi gå igennem hver enkel pixel i vores billede ved hjælp af to løkker: X og Y. For hver pixel vil vi bruge to vektor (der tilsammen udgør en segment). Disse to vektorer kalder vi for RayNear og RayFar.
RayNear vil, I vores tilfælde, altid være havde en abolut nul værdi. Denne vektor kan opfattes som kameraets eller viewportens position. Den vil altså altid ligge på præcis nul.
Vores RayFar vil altid ligge langs Z aksens negative side. Det vil dermed sige at alt der ligger i den negative del vil være foran vores kamera eller viewport og alt der ligger i den positive side vil ligge bagved. Z aksen vil altså med andre ord repræsentere dybden i vores billede, Y aksen vil repræsentere den vertikale eller lodrette akse og X aksen vil repræsentere den horisontale eller vandrette akse. Akserne kunne selvfølgelig sagtens byttes rundt så enten Y eller X akserne vil komme til at repræsentere dybden, men det er mest almindeligt at Z aksen antages for at være dybden fordi vi allerede kender X og Y akserne fra vores 2D verden.
Afstanden mellem vores RayNear og RayFar vil altid være på 1.000 enheder i denne artikel. Vi kunne sagten gøre denne afstand langt større men dette tal passer fint for vores små scener.
I renderings forløbets begyndelse vil vi lægge RayFar vektoren til venstre for vores kamera eller viewport. For hver renderet pixel vil vi så gå et skridt til højre ved hjælp af vores X løkke. Når vi er nået til enden af X løkken vil vi gå et skridt ned langs Y aksen inden vi påbegynder en ny linje af pixels. Her er en illustration set fra oven og ned.
Som du kan se på illustrationen vil Z aksen blive vores dybde og X aksen vil blive bredden. Alt geometri der befinder sig inde i den markeret trekant vil blive renderet alt udenfor vil blive ignoreret.
For at rendere en pixel bliver vi nødt til at gå igennem alt geometri i vores scene ved hjælp af en tredje løkke: J.
For hver geometri vi går igennem vil vi også skulle gå igennem alle dens trekanter ved hjælp af en fjerde og sidste løkke: I. Vi vil nu skulle bruge Interect funktionen i vektor klassen. I vores tilfælde vil vi kalde vores skæringsvektor for RayPixel. Hvis algoritmen finder en skæringspunkt i en given trekant, vil vi blot give den en farve. Funktionen ShadePixel bliver kaldt når en skæringspunkt er blevet fundet og en fragment skal tegnes eller shades. Vi kalder det geometriske stykke som kan ses gennem en pixel for en fragment. Når vi beregner en farve for en fragment kalder vi det at shade vores fragment. Funktionen returnere en farve klasse som derefter vil blive gemt i det færdig renderet billede. Renderings klassen ser således ud:
#pragma once
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
#include "Geometry.h"
#include "TGA.h"
class RENDER
{
private:
unsigned long GeometryCount;
GEOMETRY GeometryList[ 3 ];
TGAFILE Texture;
void DisplayPercent( const int Percent );
RGBA ShadePixel( VECTOR & RayNear, VECTOR & RayFar, VECTOR & Ray, VERTEX & V1, VERTEX & V2, VERTEX & V3, GEOMETRY & Geometry, TGAFILE & Texture );
public:
RENDER( void );
bool RenderImage( const unsigned short Width, const unsigned short Height, const char * Filename );
};
RENDER::RENDER( void )
{
//Lav scenen her!
}
void RENDER::DisplayPercent( const int Percent )
{
cout << Percent << "% renderet!" << endl;
}
//Denne funktion vil blive kaldt for hver gang en fragment er blevet fundet!
RGBA RENDER::ShadePixel( VECTOR & RayNear, VECTOR & RayFar, VECTOR & Ray, VERTEX & V1, VERTEX & V2, VERTEX & V3, GEOMETRY & Geometry, TGAFILE & Texture )
{
//Tegn en hvid farve
return RGBA( 1, 1, 1, 1 );
}
bool RENDER::RenderImage( const unsigned short Width, const unsigned short Height, const char * Filename )
{
int Percent = 0;
//Lav et TGA billede for vores færdige rendering
TGAFILE Image( Width, Height, false );
//Vi laver tre vektor hvoraf RayPixel vil indeholde skæringspunktet
//De to andre har vi allere været inde på
//Læg mærke til at dybden af vores ligger langs Z aksen
//Den negative akse ligger foran kameraet og den positive ligger bagved
VECTOR RayPixel;
VECTOR RayNear( 0, 0, 0, 1 );
VECTOR RayFar( 0, 0, -1000, 1 );
//Gå i gennem alle pixels på billedet!
for ( int Y = 0; Y < Image.GetHeight(); Y++ )
{
for ( int X = 0; X < Image.GetWidth(); X++ )
{
//Start i øverste venstre hjørne og afslut i nedeste højre hjørne!
RayFar.X = X - ( Image.GetWidth() / 2.0 );
RayFar.Y = Y - ( Image.GetHeight() / 2.0 );
//Sæt en dybden for hver pixel - lad altid dybden være positiv!
double Depth = -RayFar.Z;
//Gå igennem alle trekanter i vores geometri - tre vertex'er vil definér en trekant!
for ( unsigned long J = 0; J < GeometryCount; J++ )
for ( unsigned long I = 0; I < GeometryList[ J ].GetVertexCount() / 3; I++ )
{
//Hent tre vertex'er fra vores geometri
VERTEX * V1 = GeometryList[ J ].GetVertex( I * 3 + 0 );
VERTEX * V2 = GeometryList[ J ].GetVertex( I * 3 + 1 );
VERTEX * V3 = GeometryList[ J ].GetVertex( I * 3 + 2 );
//Test om vores pixel rammer noget geometri!
//Resultat vil blive RayPixel vektoren.
//Z komponenten vil definere afstanden mellem vores ramte fragment og vores kamera (Viewport)
if ( RayPixel.Intersect( RayNear, RayFar, V1->Vertex, V2->Vertex, V3->Vertex, true ) > 0 )
{
//Hvis afstanden fra vores kamera eller viewport til vores skæringspunkt er mindre end nul betyder det at skæringspunktet eller fragmenten
//befinder sig bagved vores kamera eller viewport - vi vil derfor ikke tegne det!
if ( RayPixel.Z > 0 )
{
//Hvis vores Depth er større end Z komponenten i RayPixel betyder det at der er
//sandsynlighed for at den ramte trekant vil være synligt!
if ( Depth > RayPixel.Z )
{
//Gem den nye dybde så eventuelle bagved liggende geometri ikke kommer til at ligge foran!
Depth = RayPixel.Z;
//Gem den færdige renderet pixel i billedet
RGBA Color = ShadePixel( RayNear, RayFar, RayPixel, *V1, *V2, *V3, GeometryList[ J ], Texture );
Image.SetPixel( X, Y, Color.R, Color.G, Color.B, Color.A );
}
}
}
}
}
//For hver linje vis procentdelen der er blevet renderet!
if ( static_cast<int>( static_cast<double>( Y ) / Image.GetHeight() * 100 ) != Percent )
{
Percent = static_cast<int>( static_cast<double>( Y ) / Image.GetHeight() * 100 );
DisplayPercent( Percent );
}
}
//Gem det færdige renderet billedet!
return Image.Save( Filename );
}
Vi vil nu lave en instans af renderings klassen i vores projekts hoved funktion.
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
#include "Render.h"
int main( void )
{
cout << "Lav vores render klasse!" << endl;
RENDER RayTracer;
cout << "Start rendering...!" << endl;
if ( RayTracer.RenderImage( 320, 240, "Billede.tga" ) )
cout << "Billedet er færdig renderet og er blevet gemt til din harddisk!" << endl;
else
cout << "En fejl opstod da billedet skulle gemmes!" << endl;
cout << "Tast for at afslutte programmet!" << endl;
cin.get();
return 0;
}
Som du kan se er billedet formatet sat til 320x240. Billedet vil blive gemt i samme mappe som den kompileret exe fil. Billedefilen har jeg kaldt for: Billede.tga. Den eneste klasse vi vil komme til at ændre på i vores forskellige eksempler fremover er renderings klassen. De andre klasser vil vi stort set lade være som de er skrevet. I vores første eksempel vil vi lave vi en hvid trekant der befinder sig lidt væk fra vores kamera eller viewport. Vi vil derfor lave en trekant i renderings klassens konstruktion:
RENDER::RENDER( void )
{
GeometryCount = 1;
GeometryList[ 0 ].SetVertexList( 3 );
//Vertex 1
VERTEX * V1 = GeometryList[ 0 ].GetVertex( 0 );
V1->Vertex.SetValues( -1.5, -1.0, -20.0 );
//Vertex 2
VERTEX * V2 = GeometryList[ 0 ].GetVertex( 1 );
V2->Vertex.SetValues( +1.5, -1.0, -20.0 );
//Vertex 3
VERTEX * V3 = GeometryList[ 0 ].GetVertex( 2 );
V3->Vertex.SetValues( 0.0, +1.0, -20.0 );
}
Da vi allerede har defineret en hvid farve i vores ShadePixel funktion vil vi prøve at kompile eksemplet.
Det renderede billede skulle gerne vise en hvid trekant med en sort baggrund.
Lad os lave en trekant der har forskellige vertex farver. Vi vil lave den klassiske trekant, hvor et hjørne er rød, en anden grøn og den sidste er blå. Funktionen: ShadePixel, i renderings klassen vil nu blive udvidet til følgende:
//Denne funktion vil blive kaldt for hver gang en fragment er blevet fundet!
RGBA RENDER::ShadePixel( VECTOR & RayNear, VECTOR & RayFar, VECTOR & Ray, VERTEX & V1, VERTEX & V2, VERTEX & V3, GEOMETRY & Geometry, TGAFILE & Texture )
{
//Interpoler vertex farverne
RGBA VertexColor = Geometry.InterpolateVertexColors( Ray, V1, V2, V3 );
VertexColor.Clamp( 0, 1 );
return RGBA( VertexColor.R, VertexColor.G, VertexColor.B, 1 );
}
Tilføj farverne til trekantens tre hjørner i renderings klassens konstruktion:
RENDER::RENDER( void )
{
GeometryCount = 1;
GeometryList[ 0 ].SetVertexList( 3 );
//Vertex 1
VERTEX * V1 = GeometryList[ 0 ].GetVertex( 0 );
V1->Vertex.SetValues( -1.5, -1.0, -20.0 );
V1->Color.SetValues( 1.0, 0.0, 0.0 );
//Vertex 2
VERTEX * V2 = GeometryList[ 0 ].GetVertex( 1 );
V2->Vertex.SetValues( +1.5, -1.0, -20.0 );
V2->Color.SetValues( 0.0, 1.0, 0.0 );
//Vertex 3
VERTEX * V3 = GeometryList[ 0 ].GetVertex( 2 );
V3->Vertex.SetValues( 0.0, +1.0, -20.0 );
V3->Color.SetValues( 0.0, 0.0, 1.0 );
}
Resultatet skulle nu gerne se således ud:
Denne shading metode, hvor man blander vertex farver sammen, kaldes for Gauraud shading.
Lad lægge en tekstur ovenpå trekantens flade og kombiner den med vores Gauraud shading. Jeg har lavet en lille tekstur som jeg ofte bruger i forskellige sammenhænge. Den ser således ud:
Du kan downloade billedet og ændre den til et TGA format i et billedebehandlingsprogram. Jeg kalder den blot for Test.tga Vi vil nu igen ændre lidt på vores ShadePixel funktion i renderings klassen:
//Denne funktion vil blive kaldt for hver gang en fragment er blevet fundet!
RGBA RENDER::ShadePixel( VECTOR & RayNear, VECTOR & RayFar, VECTOR & Ray, VERTEX & V1, VERTEX & V2, VERTEX & V3, GEOMETRY & Geometry, TGAFILE & Texture )
{
//Interpoler vertex farverne
RGBA VertexColor = Geometry.InterpolateVertexColors( Ray, V1, V2, V3 );
VertexColor.Clamp( 0, 1 );
//Interpoler tekstur koordinaterne
UVW Coord = Geometry.InterpolateTextureCoordinates( Ray, V1, V2, V3 );
RGBA TextureColor = Texture.GetPixel( Coord, false );
//Bland dem sammen og returner farven
return RGBA( VertexColor.R * TextureColor.R,
VertexColor.G * TextureColor.G,
VertexColor.B * TextureColor.B, 1 );
}
Vi bør nu indsætte tekstur (texture) koordinaterne til vores trekant og tilføje en TGA klasse som kan indlæse teksturen.
RENDER::RENDER( void )
{
GeometryCount = 1;
GeometryList[ 0 ].SetVertexList( 3 );
//Vertex 1
VERTEX * V1 = GeometryList[ 0 ].GetVertex( 0 );
V1->Vertex.SetValues( -1.5, -1.0, -20.0 );
V1->Color.SetValues( 1.0, 0.0, 0.0 );
V1->Coord.SetValues( 0.0, 0.0 );
//Vertex 2
VERTEX * V2 = GeometryList[ 0 ].GetVertex( 1 );
V2->Vertex.SetValues( +1.5, -1.0, -20.0 );
V2->Color.SetValues( 0.0, 1.0, 0.0 );
V2->Coord.SetValues( 1.0, 0.0 );
//Vertex 3
VERTEX * V3 = GeometryList[ 0 ].GetVertex( 2 );
V3->Vertex.SetValues( 0.0, +1.0, -20.0 );
V3->Color.SetValues( 0.0, 0.0, 1.0 );
V3->Coord.SetValues( 0.5, 1.0 );
//Indlæs teksturen for vores trekant
Texture.Load( "Test.tga" );
}
Resultatet skulle nu gerne blive til en trekant med vores tekstur (texture) lagt ovenpå.
Lad os gøre vores scene en smule mere interessant ved at bruge nogle af geometri klassens funktioner.
RENDER::RENDER( void )
{
GeometryCount = 3;
//Lav en cylinder
GeometryList[ 0 ].CreateCylinder( 20, 1, 3, 1, -1, -1, -22 );
//Lav en kasse
GeometryList[ 1 ].CreateBox( 2, 2, 2, +3, -1.5, -30 );
//Lav en plan flade
GeometryList[ 2 ].CreatePlane( 13, 13, 0, -2.5, -27 );
//Indlæs teksturen for vores trekant
Texture.Load( "Test.tga" );
}
Så langt så godt!
Nu skal vi til en mere interessant del af vores rendering. Vi vil tilføje en lyskilde til scenen!
Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.
Del også gerne artiklen med dine Facebook venner:
Kommentarer (26)
coolt
Wow!
Det er ikke hverdag man falder over en så gennemført tutorial. Du forklarer tingene rigtig godt og detaljeret uden at du forventer at folk er indforstået med hvad det lige er du mener. Jeg har arbejdet med 3D før i 3D's MAX og har tit tænkt på hvordan man kunne programmere sådan noget selv. Så du skal virkelig have mange tak for at vise mig hvordan man evt. kunne gøre det.
Keep up the good work
5/5 point
P.S. Lige et lille spørgsmål... Hvor har du alt din viden fra?
Mange tak for jeres kommentar.
Jeg har min viden fra forskellige bøger, websites og selvfølgeligt mit universitet. Jeg er netop blevet færdig som: Computer Spil Programmør på et Engelsk universitet. Da jeg altid har arbejdet med real-time rendering, har jeg i et stykke tid gået og fået lyst til at prøve software rendering. Håber at I kan drage nytte af min artikel.
Genialt! Det var lige hvad jeg havde brug for!
Søren du laver generalt DE BEDSTE artikler!
Jeg har et forslag til din vektor klasse. Jeg benytter operator overloading til ting som at addere, subtrahere og skalarproduktet. Jeg synes det gør brugen af vektorer meget mere overskuelig
Ellers thumbs up.
Jeg plejer også at bruge operator overloading i mine vektor klasser men jeg ville skrive C++ koden så den let kunne sammenlignes med Delphi koden (Se den samme artikel for Delphi:
http://www.udvikleren.dk/Delphi/Article.aspx/319/ ). Operator overloading er en forholdsvis ny feature i Delphi derfor har jeg ikke inkluderet den i Delphi versionen. I tilfælde af at en C++ programmør er interesseret i at lære Delphi (eller modsat) eller bare er interesseret i at se forskellen mellem de to sprog burde disse artikler være et godt udgangspunkt.
Mange tak for jeres kommentar. Hold jer endelig ikke tilbage hvis I har forslag, kritik eller ros
PS: Glem nu ikke rate mine artikler
Dejligt at kunne læse noget på dansk ;-) Super godt skrevet.
Hmm, god artikel, men ringe du har lavet PRÆCIS den samme artikel, bare med C++!
Undskyld men Delphi!
*Mener
Jonas:: Hvorfor ser du det som et negativt ting at have en artikel der viser implementationen i flere sprog? Skal Delphi udviklere ikke havde chancen for at implementere denne artikel?
Mange tak for rosen.
Jeg modtager gerne ris såvel som ros
Mener bare at du har lavet 2 af de samme artikler og fået dobblet point for det, du kunne bare ha' lavet det hvor artikel navnet var: "3D software rendering med C++ og Delphi"
Men synes det er ret mærkeligt
Hvorfor skulle jeg blande to sprog sammen når de er delt i to adskilte teknologier på dette forum? I så fald hvilken udviklings teknologi skulle jeg så poste dem under?
Er det fordi du mener at jeg har fået for mange points for de artikler?
Hvorfor skulle jeg blande to sprog sammen når de er delt i to adskilte teknologier på dette forum? I så fald hvilken udviklings teknologi skulle jeg så poste dem under?
Er det fordi du mener at jeg har fået for mange points for de artikler?
Jep
Jamen så må du jo snakke med de admins der har ansvar for artikeler her på forummet og tage problemstilling op hos dem. Det kan jeg næsten ikke gøre noget ved. Håber trods alt du kan bruge mine artikler til noget.
Det kan jeg da
SIKKERT! Artiklen er meget god
Men synes bare det er urætfærdigt at man får Up for begge
Jonas:
De der UP, er det ikke bare nogle tal, og så ikke mere i det?
Altså jeg laver altid Forumindlæg uden point..
Hey. Virkelig gennemført beskrivelse, og tror bestemt at jeg skal ha undersøgt mere på emnet.
Anyway. Har et par spørgsmål:
Den første kodestump på s. 4, som starter således: "//Den officelle TGA fil header!". Skal den gemmes som TGA.h? For hvis det er tilfældet så prøver den jo på at include sig selv
Og kan det her på udvikleren lade sig gøre at hente de filer som bliver brugt/lavet i artiklerne? For det ville da være en fordel hvis man sidder ligesom mig og roder rundt i hvordan det hele skal sidde sammen så man har en virkende bund at starte på, og at man ved fejl evt. ved fejl ved at det ikke er koden, men noget setup
Og til sidst. Dette burdet ikke have noget problem med at compile på en ubuntu-installation?
mvh. martin
Hej Martin,
Stukturen som jeg har kaldt for: FILEHEADER, er den officielle TGA fil header. Den skal ligge øverst i header filen TGA.h således den kan genkendes af TGA klassen. Du kan godt lægge den i en separat header fil, hvis du har lyst til det, men så skal den selvfølgelig inkluderes i TGA header filen inden TGA klassen defineres. Jeg vil nok anbefale at lægge den inden i selve TGA klassen da denne TGA fil format kun benyttes af TGA klassen! Med andre ord så er alt koden i den refererede tekst boks implementeringen af hele TGA klassen og kan skrives i en header fil eller en header med tilhørende cpp fil.
Angående kilde kode, du kan sende mig en udvikler post her på forummet med din mail adresse. Så vil jeg sende projektet til dig i en zip fil.
Alle andre er selvfølgelig også velkommen til at sende mig deres mail adresse.
Projektet skulle være platforms uafhængig. Der bliver ikke brugt nogle operative kommandoer eller API'er af nogen form, så ja, hvis din kompiler ellers kan kompile ganske almindelige console applikationer, så burde du også kunne kompilere dette projekt. Lad mig vide hvis du støder ind i nogle problemer.
Søren Klit Lambæk
Rigtig god artikel.
Du har dog glemt at fortælle at man skal huske at ændre RenderImage til at bruge den nye ShadePixel()
RGBA Color = ShadePixel(RayNear, RayFar, RayPixel, *V1, *V2, *V3, GeometryList[J], Texture, PointLight);
Og Phong laver compile errors
Da du her
RGBA VertexColor = Geometry.InterpolateVertexColors(Ray, V1, V2, V3);
parser V1, V2 og V3 til InterpolateVertexColors funktionen. Den ta'r vectors, men V1, V2 og V3 er vertexes.
Eller... Nu bliver jeg forvirret.
Error 1 error C2664: 'GEOMETRY::InterpolateNormals' : cannot convert parameter 2 from 'VECTOR' to 'VERTEX &' renderer.cpp 91
Nej, okay. er omvendt. Funktionen forventer vertexes, men V1, V2 og V3 er vectors.
Hvordan den være?
Denne side er da håbløst bugged.
Hvordan [b]skal[/b] den være?
Hej Morten, jeg er ikke helt med paa hvad du mener. Baade InterpolateNormals() og InterpolateColors() tager vertexer (Vertices). Kan du ikke give mig noget mere sammenhaengene kode saa jeg kan se mere detaljeret hvad du goer. Tak
Du skal være
logget ind for at skrive en kommentar.