| Vous savez que vous avez fait trop de raytracing quand ... |
| ... On vous demande comment vous avez fait cette chose, de la part de l'auteur du raytraceur que vous avez utilisé. |
| -- Alex McLeod |
Un raytraceur fait avec POV-Ray semble vraiment fou, non ? Qu'est-ce que c'est après tout ? POV-Ray est déjà un raytraceur lui-même, comment l'utiliser pour faire un raytraceur ? Qu'est-ce que...?
L'idée est de faire un simple raytraceur de sphères qui supporte les sphères colorées (avec illumination diffuse et spéculaire), les sources de lumière colorées, les réflexions et les ombres avec le SDL de POV-Ray (langage de description de scène), puis juste rendre l'image créée de cette façon. Pour cela, nous n'utiliserons pas POV-Ray pour tracer les sphères, mais nous ferons notre propre raytraceur avec le SDL et utiliserons la partie génératrice de POV-Ray pour juste avoir l'image à l'écran.
Quelle idée obscure se cache derrière cette folie ? Pourquoi ne pas utiliser POV-Ray lui-même pour tracer les sphères plus vite ?
L'idée n'est pas la rapidité ou la qualité, mais de montrer la puissance du SDL. Si vous savez comment faire une telle chose, comme le fait un raytraceur, vous pouvez vraiment vous rendre compte de la puissance de SDL.
L'idée de ce document est de faire une approche différente de l'apprentissage du SDL de POV-Ray. Il est fait pour être un type différent de cours : au lieu de commencer avec les bases en donnant des exemples simples, nous sautons dans le code final de SDL et regardons comment il est fait. Bien sûr, c'est fait de manière à ce que même les débutants puissent en apprendre quelque chose.
Un autre avantage est que vous apprendrez comment un raytraceur de sphère est simplement fait. Il y a beaucoup de fausses idées sur le traçage, et savoir comment en faire un en élimine beaucoup.
Bien sûr, ce cours essaie de commencer avec les bases, il ira relativement vite vers des scripts très pointus, aussi il peut ne pas être le meilleur cours pour un vrai débutant, mais avoir des connaissances de base est suffisant. De même, des utilisateurs plus aguérris pourront trouver des informations ici.
Note : parfois, des mathématiques sont nécessaires, aussi vous ne devez pas être effrayés par les mathématiques.
Si quelque syntaxe SDL de POV-Ray n'est pas claire, vous devez consulter la documentation de POV-Ray pour plus d'aide. Ce cours explique comment elle peut être utilisée, pas ce qu'est sa syntaxe.
L'idée est de tracer une simple scène consistant en sphères et sources de lumière dans une aire en deux dimensions contenant des vecteurs de couleur qui représentent notre 'écran'.
Après cela, nous avons juste à poser ces couleurs dans la scène actuelle pour que POV-Ray les trace. Cela est fait avec la création d'une maillage de triangles plat. Le maillage est plat comme un plan avec une palette de couleurs dessus. Nous pouvions aussi bien écrire le résultat dans un format comme PPM, puis le lire et l'appliquer comme un image sur le plan, mais ainsi nous évitons un fichier temporaire.
L'image suivante est faite avec le raytraceur SDL. Il a calculé l'image à une résolution de 160x120 pixels, puis en a tracé une image de 512x384. Cela fait que l'image est abimée (parcequ'elle est partiquement "zoomée" par un facteur de 3.2). Le calcul à 320x240 donne un meilleur résultat, mais c'est aussi plus lent :

Note : il n'y a ni sphère réelle, ni source de lumière réelle ici ("réelle" selon le point de vue de POV-Ray), juste un maillage de triangles coloré (comme un plan couvert de pigment) et une caméra, rien d'autre.
Voici le code source du raytraceur; nous le regarderons morceau par morceau à travers ce cours.
#declare ImageWidth = 160; #declare ImageHeight = 120; #declare MaxRecLev = 5; #declare AmbientLight = <.2, .2, .2>; #declare BGColor = <0, 0, 0>; // Information sur la shère. // Les valeurs sont : // Center,, Color, #declare Coord = array[5][4] {{<-1.05, 0, 4>, <1, .5, 0>, <1, .5, .25>, <40, .8, 0>} {<1.05, 0, 4>, <1, .5, 0>, <.5, 1, .5>, <40, .8, 0>} {<0,-3, 5>, <2, .5, 0>, <.25, .5, 1>, <30, .4, 0>} {<-1, 2.3, 9>, <2, .5, 0>, <.5, .3, .1>, <30, .4, 0>} {<1.3, 2.6, 9>, <1.8, .5, 0>, <.1, .3, .5>, <30, .4, 0>} } // Directions de la source de lumière et les couleurs : #declare LVect = array[3][2] {{<-1, 0,-.5>, <.8, .4, .1>} {<1, 1,-.5>, <1, 1, 1>} {<0, 1, 0>, <.1, .2, .5>} } //===================== // Calculs de traçage : //===================== #declare MaxDist = 1e5; #declare ObjAmnt = dimension_size(Coord, 1); #declare LightAmnt = dimension_size(LVect, 1); #declare Ind = 0; #while(Ind < LightAmnt) #declare LVect[Ind][0] = vnormalize(LVect[Ind][0]); #declare Ind = Ind+1; #end #macro calcRaySphereIntersection(P, D, sphereInd) #local V = P-Coord[sphereInd][0]; #local R = Coord[sphereInd][1].x; #local DV = vdot(D, V); #local D2 = vdot(D, D); #local SQ = DV*DV-D2*(vdot(V, V)-R*R); #if(SQ < 0) #local Result = -1; #else #local SQ = sqrt(SQ); #local T1 = (-DV+SQ)/D2; #local T2 = (-DV-SQ)/D2; #local Result = (T1<T2 ? T1 : T2); #end Result #end #macro Trace(P, D, recLev) #local minT = MaxDist; #local closest = ObjAmnt; // Recherche de l'intersection la plus proche : #local Ind = 0; #while(Ind < ObjAmnt) #local T = calcRaySphereIntersection(P, D, Ind); #if(T>0 & T<minT) #local minT = T; #local closest = Ind; #end #local Ind = Ind+1; #end // Si pas trouvée, retourne la couleur de l'arrière-plan : #if(closest = ObjAmnt) #local Pixel = BGColor; #else // Sinon calcul la couleur du point d'intersection : #local IP = P+minT*D; #local R = Coord[closest][1].x; #local Normal = (IP-Coord[closest][0])/R; #local V = P-IP; #local Refl = 2*Normal*(vdot(Normal, V)) - V; // Eclairage : #local Pixel = AmbientLight; #local Ind = 0; #while(Ind < LightAmnt) #local L = LVect[Ind][0]; // Test d'ombre : #local Shadowed = false; #local Ind2 = 0; #while(Ind2 < ObjAmnt) #if(Ind2!=closest & calcRaySphereIntersection(IP,L,Ind2)>0) #local Shadowed = true; #local Ind2 = ObjAmnt; #end #local Ind2 = Ind2+1; #end #if(!Shadowed) // Diffuse: #local Factor = vdot(Normal, L); #if(Factor > 0) #local Pixel=Pixel+LVect[Ind][1]*Coord[closest][2]*Factor; #end // Spéculaire : #local Factor = vdot(vnormalize(Refl), L); #if(Factor > 0) #local Pixel = Pixel + LVect[Ind][1]*pow(Factor, Coord[closest][3].x)* Coord[closest][3].y; #end #end #local Ind = Ind+1; #end // Réflexion: #if(recLev < MaxRecLev & Coord[closest][1].y > 0) #local Pixel = Pixel + Trace(IP, Refl, recLev+1)*Coord[closest][1].y; #end #end Pixel #end #debug "Rendering...\n\n" #declare Image = array[ImageWidth][ImageHeight] #declare IndY = 0; #while(IndY < ImageHeight) #declare CoordY = IndY/(ImageHeight-1)*2-1; #declare IndX = 0; #while(IndX < ImageWidth) #declare CoordX = (IndX/(ImageWidth-1)-.5)*2*ImageWidth/ImageHeight; #declare Image[IndX][IndY] = Trace(-z*3, <CoordX, CoordY, 3>, 1); #declare IndX = IndX+1; #end #declare IndY = IndY+1; #debug concat("\rDone ", str(100*IndY/ImageHeight, 0, 1), "% (line ", str(IndY, 0, 0), " out of ", str(ImageHeight, 0, 0), ")") #end #debug "\n" //====================================== // Création de l'image (maillage coloré) : //====================================== #default {finish {ambient 1}} #debug "Creating colored mesh to show image...\n" mesh2 { vertex_vectors { ImageWidth*ImageHeight*2, #declare IndY = 0; #while(IndY < ImageHeight) #declare IndX = 0; #while(IndX < ImageWidth) <(IndX/(ImageWidth-1)-.5)*ImageWidth/ImageHeight*2, IndY/(ImageHeight-1)*2-1, 0>, <((IndX+.5)/(ImageWidth-1)-.5)*ImageWidth/ImageHeight*2, (IndY+.5)/(ImageHeight-1)*2-1, 0> #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end } texture_list { ImageWidth*ImageHeight*2, #declare IndY = 0; #while(IndY < ImageHeight) #declare IndX = 0; #while(IndX < ImageWidth) texture {pigment {rgb Image[IndX][IndY]}} #if(IndX < ImageWidth-1 & IndY < ImageHeight-1) texture {pigment {rgb (Image[IndX][IndY]+Image[IndX+1][IndY]+ Image[IndX][IndY+1]+Image[IndX+1][IndY+1])/4}} #else texture {pigment {rgb 0}} #end #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end } face_indices { (ImageWidth-1)*(ImageHeight-1)*4, #declare IndY = 0; #while(IndY < ImageHeight-1) #declare IndX = 0; #while(IndX < ImageWidth-1) <IndX*2+ IndY *(ImageWidth*2), IndX*2+2+IndY *(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ IndY *(ImageWidth*2), IndX*2+2+IndY *(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2), <IndX*2+ IndY *(ImageWidth*2), IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ IndY *(ImageWidth*2), IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2), <IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2), <IndX*2+2+IndY *(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+2+IndY *(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2) #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end } } camera {orthographic location -z*2 look_at 0}
Avant de commencer à regarder le code, jetons un oeil sur le fonctionnement du raytraceur. Cela nous aidera à comprendre ce que fait le script.
L'idée de base du traçage est de "tirer" des rayons depuis la caméra à travers la scène et de voir ce que le rayon touche. Si le rayon touche la surface d'un objet, alors des calculs de luminosité sont faits dans le but de trouver la couleur de la surface en ce point.
L'image suivante montre cela graphiquement :

D'abord un rayon est "tiré" dans une direction spécifique pour voir s'il y a quelque chose par là. Comme c'est résolu mathématiquement, nous avons besoin de connaître la représentation mathématique du rayon et des objets dans la scène, de manière à pouvoir calculer le lieu d'intersection du rayon sur l'objet. Une fois que nous avons tous les points d'intersection, nous choisissons le plus proche.
Après cela, nous avons à calculer l'illumination de l'objet au point d'intersection. Dans le modèle le plus basique d'illumination (comme celui de ce script) il y a trois choses pincipales qui affectent l'illumination de la surface :
Ne vous inquiétez pas si cela paraît confus. Des détails complets de toutes ces choses seront donnés à travers ce cours, pendant que nous regarderons ce que fait le script de traçage. La chose la plus importante de cette étape est de comprendre comment l'algorithme de base du traçage fonctionne au niveau théorique (l'image du dessus en dit long là dessus).
Regardons le code source du raytraceur ligne par ligne et voyons ce qu'il fait.
#declare ImageWidth = 160; #declare ImageHeight = 120; #declare MaxRecLev = 5; #declare AmbientLight = <.2, .2, .2>; #declare BGColor = <0, 0, 0>;
Ces lignes déclarent quelques identifiants définissant quelques valeurs générales qui seront utilisées dans le code à venir. Le mot clé que nous utilisons ici est #declare et il signifie que nous déclarons un identifiant global, qui doit être vu dans tout le code.
Comme nous le voyons, nous déclarons certains identifiants pour être de type numérique, et d'autres de type vectoriel. Les identifiants de type vectoriel sont, en fait, utilisés plus tard pour la définition des couleurs (comme l'indiquent leurs noms).
Le ImageWidth et le ImageHeight définissent la résolution de l'image que nous allons rendre.
Note : cela définit seulement la résolution de l'image que nous allons rendre dans le SDL (c.a.d. dans l'aire que nous définirons plus tard); il ne définit pas la résolution de l'image que rendra POV-Ray.
Le MaxRecLev limite le nombre maximum de réflexions récursives calculées par le code. C'est l'équivalent de la valeur de max_trace_level dans le global_settings que POV-Ray utilise pour la génération.
Le AmbientLight définit une couleur qui est ajoutée à toutes les surfaces. Cela aboutit à un "éclairage" des parties ombrées pour qu'elles ne soient pas totalement noires. C'est équivalent à la valeur de ambient_light dans le global_settings.
Enfin, BGColor définit la couleur des rayons qui ne heurtent rien. C'est équivalent au bloc background de POV-Ray.
// Information de la sphère. // Les valeurs sont : // Center,, Color, #declare Coord = array[5][4]{ {<-1.05, 0, 4>, <1, .5, 0>, <1, .5, .25>, <40, .8, 0>} {<1.05, 0, 4>, <1, .5, 0>, <.5, 1, .5>, <40, .8, 0>} {<0,-3, 5>, <2, .5, 0>, <.25, .5, 1>, <30, .4, 0>} {<-1, 2.3, 9>, <2, .5, 0>, <.5, .3, .1>, <30, .4, 0>} {<1.3, 2.6, 9>, <1.8, .5, 0>, <.1, .3, .5>, <30, .4, 0>} } // directions de la source de lumière et les couleurs : #declare LVect = array[3][2]{ {<-1, 0,-.5>, <.8, .4, .1>} {<1, 1,-.5>, <1, 1, 1>} {<0, 1, 0>, <.1, .2, .5>} }
Ici nous utilisons des déclarations plus complexes : déclarations de tableaux.
En fait, c'est plus complexe que des tableaux simples, puisque nous avons déclaré des tableaux à deux dimensions.
Un simple tableau à une dimension peut être déclaré comme :
#declare MyArray = array[4]{1, 2, 3, 4}
et les valeurs peuvent y être lues avec par exemple : MyArray[2] (qui retournera 3 dans ce cas car l'indexation commence à 0, c.a.d. que l'index 0 donne la première valeur du tableau).
Un tableau à deux dimensions peut être vu comme un tableau contenant un tableau. Donc, si vous dites array[3][2], cela signifie "un tableau qui a trois éléments, chacun contenant un tableau de deux éléments". Quand vous voulez lire une de ses valeurs, par exemple MyArray[1][3], vous pouvez penser à cela comme "prendre la quatrième valeur du deuxième tableau" (puisque l'indexation commence à 0, alors l'index de valeur 3 désigne la quatrième valeur).
Note : bien sûr, vous pouvez tout poser dans un tableau (numériques, vecteurs, objets et autres choses) mais vous ne pouvez poser qu'un type de chose dans un même tableau. Donc vous ne pouvez pas mélanger des valeurs numériques et des objets dans le même tableau. (Une jolie caractéristique est que tous les objets de POV-Ray sont considérés comme équivalents, ce qui signifie qu'un tableau d'objets peut contenir toute sorte d'objet.)
Ce que nous faisons ici est la définition des informations pour nos sphères et sources de lumière. Le premier tableau (appelé Coord) définit l'information pour les sphères, et le second (LVect) définit les sources de lumière.
Pour les sphères, nous définissons leur centre comme premier vecteur. Le second vecteur a, à la fois, son rayon et la quantité de réflexion (qui est équivalente à la valeur de reflection dans le bloc finition d'un objet). C'est un truc utilisé pour ne pas perdre trop de place, ainsi nous utilisons deux valeurs d'un même vecteur pour définir deux choses différentes.
Le troisième vecteur définit la couleur de la sphère, et le quatrième s'occupe du composant spéculaire de la luminosité (équivalent aux valeurs phong_size et phong dans le bloc finition d'un objet).
Le tableau de définition de la source de lumière contient les vecteurs de direction et les couleurs. Cela signifie que les sources de lumière sont directionnelles, donc, ils disent seulement de quelle direction provient la lumière. Il aurait été tout aussi simple de faire des sources ponctuelles, bien sûr.
Nous utiliserons l'information de ces tableaux plus tard, dans le but de tracer la scène qu'ils définissent.
#declare MaxDist = 1e5; #declare ObjAmnt = dimension_size(Coord, 1); #declare LightAmnt = dimension_size(LVect, 1); #declare Ind = 0; #while(Ind < LightAmnt) #declare LVect[Ind][0] = vnormalize(LVect[Ind][0]); #declare Ind = Ind+1; #end
Avant d'être capable de lancer le raytraceur, nous devons initialiser un couple de choses.
Le MaxDist définit la distance maximale à laquelle une surface peut être de l'origine du rayon. Cela veut dire que si une surface est plus loin que cette valeur, de l'origine du rayon, elle ne sera pas vue. En fait, cette valeur est inutile et vous pouvez faire le raytraceur sans une telle limitation, mais nous évitons une phase supplémentaire en faisant cela, et pour les scènes de la taille de la nôtre, cela n'a pas d'importance. (Si vous voulez vraiment, mais vraiment, vous débarrasser de la limitation, je suis sûr que vous saurez comment le faire après ce cours.)
Les identifiants ObjAmnt et LightAmnt sont déclarés juste pour nous faciliter le comptage des objets et des sources de lumières présentes (nous avons besoin de cette information pour passer sur tous les objets et sources de lumière). L'appel de la fonction dimension_size() est une bonne manière d'obtenir le nombre d'éléments d'un tableau.
Parfait, maintenant nous arrivons à quelque chose de plus avancé : que fait la boucle 'while' ici ?
La boucle #while utilise l'identificateur Ind comme une valeur d'index allant de 0 à LightAmnt-1 (oui, -1; quand Ind prend la valeur LightAmnt la boucle est immédiatement fermée). Nous voyons aussi que nous avons indexé le tableau LVect; aussi, c'est clair que nous passons à travers toutes les sources de lumière (surtout à travers leurs vecteurs de direction, puisque nous n'utilisons que la partie [0]) et nous leur assignons quelque chose.
Ce que nous faisons est l'assignation d'une version normalisée de chaque direction de source de lumière sur elle-même, donc, que nous les normalisons seulement.
Normaliser est un synonyme de "convertir en vecteur unitaire", donc, convertir en un vecteur avec la même direction que l'original, mais avec la longueur 1.
Pourquoi ? Nous verrons plus tard que, pour les calculs d'illumination, nous aurons besoin de vecteurs unitaires. C'est plus efficace de convertir les directions de la source de lumière au début qu'à chaque pixel à venir.
#macro calcRaySphereIntersection(P, D, sphereInd) #local V = P-Coord[sphereInd][0]; #local R = Coord[sphereInd][1].x; #local DV = vdot(D, V); #local D2 = vdot(D, D); #local SQ = DV*DV-D2*(vdot(V, V)-R*R); #if(SQ < 0) #local Result = -1; #else #local SQ = sqrt(SQ); #local T1 = (-DV+SQ)/D2; #local T2 = (-DV-SQ)/D2; #local Result = (T1 < T2 ? T1 : T2); #end Result #end
Voici le coeur de tout le processus de traçage.
D'abord voyons comment la macro fonctionne (si vous le savez, sautez cette section) :
Une macro fonctionne comme une commande de substitution (similaire à la macro #define dans le langage de programmation C). Le corps de la macro est en pratique inséré à l'endroit où la macro est appelée. Par exemple, vous pouvez utiliser une macro comme ceci :
#macro UnitSphere() sphere {0, 1} #end object {UnitSphere() pigment {rgb 1}}
Le résultat de ce code est, en fait, comme si vous aviez écrit :
object {sphere {0, 1} pigment {rgb 1}}
Bien sûr, il n'y a aucune raison de faire cela, puisque vous pouvez avoir #declared le UnitSphere comme une sphère de rayon 1. Toutefois, le pouvoir des macros se révèle quand vous commencez à utiliser les paramètres de macros. Par exemple :
#macro Sphere(Radius) sphere {0, Radius} #end object {Sphere(3) pigment {rgb 1}}
Maintenant, vous pouvez utiliser la macro Sphere pour créer une sphère avec un rayon spécifique. Bien sûr, cela n'a pas plus de sens, puisque vous pouvez juste écrire la primitive sphère directement parce qu'elle est très courte, mais cet exemple est intentionnellement court pour vous montrer comment ça fonctionne; les macros deviennent très utiles quand elles construisent quelque chose de beaucoup plus compliqué qu'une sphère.
Il y a une différence importante entre les macros de POV-Ray et les vraies macros de substitution : toute déclaration #local dans la macro ne sera vue que d'elle, donc, elle n'aura aucun effet sur l'environnement où est appelée la macro. L'exemple suivant montre de quoi je parle :
#macro Sphere(Radius) #local Color = <1, 1, 1>; sphere {0, Radius pigment {rgb Color}} #end #declare Color = <1, 0, 0>; object {Sphere(3)} // 'Color' est toujours <1, 0, 0> ici, // donc la boîte suivante sera rouge : box {-1, 1 pigment {rgb Color}}
Dans l'exemple précédent, bien que la macro crée un identificateur local appelé Color et qu'il y a un identificateur de même nom au niveau global, la déclaration locale n'affecte pas la globale. Et même s'il n'y aucune définition globale de Color, celle interne à la macro ne sera pas vue depuis l'extérieur.
Il y a une exception importante à cela, c'est une des caractéristiques les plus importantes des macros (merci au fait qu'elles puissent alors être utilisées comme des fonctions) : si un identificateur (local ou global) apparaît seul dans le corps d'une macro (habituellement à la fin), sa valeur sera passée à l'extérieur de la macro (comme si c'était une valeur retournée). L'exemple suivant montre comment cela fonctionne :
#macro Factorial(N) #local Result = 1; #local Ind = 2; #while(Ind <= N) #local Result = Result*Ind; #local Ind = Ind+1; #end Result #end #declare Value = Factorial(5);
Bien que l'identificateur Result soit local à la macro, sa valeur est passée comme si c'était une valeur retournée parce qu'elle est à la dernière ligne de la macro (où Result apparaît seul) et donc l'identificateur Value sera mis à la factoriale de 5.
Revoici la macro du début de la page pour que vous n'ayez pas besoin de paginer :
#macro calcRaySphereIntersection(P, D, sphereInd) #local V = P-Coord[sphereInd][0]; #local R = Coord[sphereInd][1].x; #local DV = vdot(D, V); #local D2 = vdot(D, D); #local SQ = DV*DV-D2*(vdot(V, V)-R*R); #if(SQ < 0) #local Result = -1; #else #local SQ = sqrt(SQ); #local T1 = (-DV+SQ)/D2; #local T2 = (-DV-SQ)/D2; #local Result = (T1 < T2 ? T1 : T2); #end Result #end
L'idée derrière cette macro est de prendre un point de départ (c.a.d. le point de départ du rayon), un vecteur de direction (la direction de tir du rayon) et un index à la définition de la sphère définie auparavant. La macro retourne une valeur de facteur; cette valeur exprime le nombre de multiplications du vecteur de direction dans le but de toucher la sphère.
Cela signifie que si le rayon touche la sphère, le point d'intersection sera à :StartingPoint + Result*Direction
La valeur retournée peut être négative, ce qui signifie que le point d'intersection était derrière le point de départ. Une valeur négative sera seulement ignorée, comme si le rayon n'avait rien touché. Nous pouvons utiliser cela pour un petit truc (qui semble évident quand il est dit, mais pas si évident si vous devez le trouver seul) : si le rayon ne touche aucune sphère, nous retournons seulement une valeur négative (peu importe la raison).
Et comment la macro fait cela ? Qu'elle est la théorie derrière cette expression complexe de mathématiques ?
J'utiliserai une syntaxe similaire à celle de POV-Ray pour exprimer les formules mathématiques parce que c'est probablement la manière la plus simple de le faire.
Utilisons les lettres suivantes :
P = Point de départ du rayon
D = Direction du rayon
C = Centre de la sphère
R = Rayon de la sphère
La théorie derrière la macro est que nous voulons connaître la valeur T qui respecte cela :
vlength(P+T*D-C) = R
Cela signifie : la longueur du vecteur entre le centre de la sphère (C) et le point d'intersection (P+T*D) est égal au rayon (R).
Si nous utilisons une autre lettre telle que :
V = P-C
alors la formule est réduite à :
vlength(T*D+V) = R
qui nous facilite la vie. Cette formule peut être déployée en :
(T*Dx+Vx)2 + (T*Dy+Vy)2 + (T*Dz+Vz)2 - R2 = 0
Résoudre T de là est trivial. Nous obtenons un polynôme de second degré qui a deux solutions (j'utiliserai le symbole "·" pour représenter le produit de deux vecteurs) :
T = (-D·V ± sqrt((D·V)2 - D2(V2-R2))) / D2
Note : D2 signifie actuellement D·D
Quand le discréminant (l'expression dans la racine carrée) est négatif, le rayon ne touche pas la sphère, et nous pouvons retourner une valeur négative (la macro retourne -1). Nous devons vérifier cela pour éviter l'erreur racine carrée d'une valeur négative; comme cela a une signification très logique dans ce cas, la vérification est naturelle.
Si la valeur est positive, il y a deux solutions (ou seulement une si la valeur est zéro, mais cela n'a pas d'importance ici), qui correspondent aux deux points d'intersection du rayon avec la sphère.
Comme nous avons deux valeurs, nous devons retourner la plus petite des deux (l'intersection la plus proche). C'est ce que fait cette portion de code :
#local Result = (T1<T2 ? T1 : T2);
Note : ceci est un algorithme incomplet : si une valeur est négative et l'autre positive (cela arrive quand le point de départ est dans une sphère), nous devons retourner la valeur positive. Cela est résolu par le fait que nous ne voyons pas la surface interne de la sphère quand nous plaçons la caméra à l'intérieur.
Pour notre simple scène c'est suffisant puisque nous ne plaçons pas la caméra dans une sphère et nous n'avons pas de sphères transparentes. Nous pouvons ajouter une vérification qui regarde si une des valeurs est positive et l'autre négative, de manière à ne retourner que la positive. toutefois, cela donne un résultat bizarre (vous pouvez essayer si vous voulez). Cela est probablement dû au manque de précision des nombres à virgule flottante et se produit lors du calcul des réflexions (le point de départ est exactement sur la surface de la sphère). Vous pouvez corriger ce problème en utilisant les valeurs epsilon pour être débarrassé des problèmes de précision, mais dans notre scène simple cela ne sera pas nécessaire.
#macro Trace(P, D, recLev)
Si la macro d'intersection rayon-sphère est le coeur du raytraceur, alors la macro Trace est pratiquement tout le reste, le "corps" du raytraceur.
La macro Trace est une macro qui prend le point de départ du rayon, la direction du rayon et une compteur de récursion (qui devra toujours être à 1 lors d'un appel de la macro de l'extérieur; 1 pourrait être sa valeur par défaut si POV-Ray supportait les valeurs par défaut pour les paramètres de macro). Elle calcule et retourne une couleur pour le rayon.
C'est la macro que nous appelons pour chaque pixel que nous voulons calculer. Donc, le point de départ du rayon est la position de notre caméra et la direction est la direction du rayon partant de là et passant à travers le "pixel" que nous calculons. La macro renvoie la couleur de ce pixel.
Ce que fait la macro est de voir quelle sphère (s'il y en a une) est heurtée par le rayon, puis elle calcule l'illumination pour ce point d'intersection (ce qui inclut le calcul de la réflexion), et renvoie une couleur.
La macro Trace est récursive, signifiant qu'elle s'appelle elle-même. Plus spécifiquement, elle s'appelle quand elle veut calculer le rayon reflété depuis la surface d'une sphère. La valeur de recLev est utilisée pour arrêter cette récursivité quand le niveau maximum est atteint (elle calcule la réflexion que si recLev < MaxRecLev).
Examinons cette relativement longue macro point par point :
#local minT = MaxDist; #local closest = ObjAmnt; // Cherche l'intersection la plus proche : #local Ind = 0; #while(Ind < ObjAmnt) #local T = calcRaySphereIntersection(P, D, Ind); #if(T > 0 & T < minT) #local minT = T; #local closest = Ind; #end #local Ind = Ind+1; #end
Un rayon peut heurter plusieurs sphères et nous avons besoin de l'intersection la plus proche (et la sphère concernée). On pourrait penser que ce calcul est assez complexe, demandant le tri de tous les points d'intersection. Toutefois, c'est assez simple, comme le montre le code au-dessus.
Si vous vous souvenez de la partie précédente, la macro de l'intersection rayon-sphère renvoie un facteur de multiplication du vecteur de direction dans le but de retrouver le point d'intersection. Ce que nous faisons est juste de rappeler la macro d'intersection rayon-sphère pour chaque sphère et prenons la plus petite valeur retournée (qui est plus grande que zéro).
D'abord, nous initialisons l'identifiant minT, qui retiendra cette plus petite valeur (c'est ici que nous avons besoin de la valeur MaxDist, toutefois, la modification de ce code pour fonctionner avec cette limitation est triviale et laissée à l'utilisateur). Puis nous allons vers toutes les sphères et appelons la macro d'intersection pour chacune d'elle. Puis nous regardons si la valeur retournée est plus grande que 0 et plus petite que minT, et si c'est le cas, nous assignons cette valeur à minT. Quand la boucle s'arrête, cet identifiant contient le point d'intersection le plus petit.
Note : nous assignons aussi l'index de la sphère qui a cette intersection dans l'identifiant closest.
Ici, nous usons d'un truc, et c'est relatif à sa valeur initiale : ObjAmnt. Pourquoi l'avons nous initialisé à ça ? Le but était d'initialiser avec une valeur qui n'est pas un index valide pour une sphère (ObjAmnt n'est pas un index autorisé puisque les indices vont de 0 à ObjAmnt-1); une valeur négative aurait aussi bien marché, cela n'a pas d'importance. Si le rayon ne touche aucune sphère, alors cet identifiant n'est pas modifié et nous pouvons passer.
// Si pas trouvée, retourne la couleur d'arrière-plan : #if(closest = ObjAmnt) #local Pixel = BGColor;
Si le rayon ne touche aucune sphère, nous retournons la couleur de l'arrière-plan (définie par l'identifiant BGColor).
Maintenant commence une des parties les plus intéressantes du processus de traçage : comment calcule-t-on la couleur du point d'intersection ?
D'abord nous devons précalculer un couple de choses :
#else // sinon calcule la couleur du point d'intersection : #local IP = P+minT*D; #local R = Coord[closest][1].x; #local Normal = (IP-Coord[closest][0])/R; #local V = P-IP; #local Refl = 2*Normal*(vdot(Normal, V)) - V;
Naturellement, nous avons besoin du point d'intersection lui-même (nécessaire pour calculer le vecteur de la normale et comme point de départ du rayon réfléchi). Cela est calculé dans l'identifiant IP avec la formule que j'ai répétée un certain nombre de fois dans ce cours.
Puis nous avons besoin du vecteur de la normale de la surface au point d'intersection. Un vecteur de normale est un vecteur perpendiculaire (à 90 degrés) à la surface. Pour une sphère, c'est très simple à calculer : c'est le vecteur reliant le centre de la sphère au point d'intersection.
Note : nous le normalisons (convertissons en un vecteur unitaire, c.a.d. un vecteur de longueur 1) en le divisant par le rayon de la sphère. Le vecteur de la normale doit être normalisé pour les calculs de lumière.
Maintenant un truc : nous avons besoin de la direction du rayon réfléchi. Ce vecteur est nécessaire pour calculer le rayon réfléchi, mais il est aussi nécessaire pour l'illumination spéculaire.
Cela est calculé dans l'identifiant Refl dans le code au-dessus. Ce que nous devons faire est de prendre le vecteur allant du point d'intersection au point de départ (P-IP) et de le "répliquer" par rapport au vecteur de la normale. La formule pour la "réplication" d'un vecteur V au-delà d'un vecteur unitaire (appelons-le Axis) est :
MirroredV = 2*Axis*(Axis·V) - V
(Nous pouvons voir la théorie derrière cette formule en détail, mais n'allons pas trop loin dans les mathématiques avec ce cours, devons-nous ?)
// Eclairage : #local Pixel = AmbientLight; #local Ind = 0; #while(Ind < LightAmnt) #local L = LVect[Ind][0];
Maintenant, nous pouvons calculer l'illumination au point d'intersection. Pour cela nous devons vérifier toutes les sources de lumière.
Note : L contient le vecteur de direction qui pointe vers la source de lumière, non sa position.
Nous initialisons aussi la couleur à renvoyer (Pixel) avec la valeur de la lumière ambiante (donnée dans la partie des paramètres globaux). Le but est de lui ajouter les couleurs (les couleurs viennent de l'illumination diffuse et spéculaire, et de la réflexion).
La toute première chose à faire pour la calcul de la luminosité d'une source de lumière est de voir si elle illumine le point d'intersection (c'est une des plus belles caractéristiques du traçage : les calculs d'ombre sont risiblement faciles à faire) :
// Test d'ombre: #local Shadowed = false; #local Ind2 = 0; #while(Ind2 < ObjAmnt) #if(Ind2!=closest & calcRaySphereIntersection(IP,L,nd2) > 0) #local Shadowed = true; #local Ind2 = ObjAmnt; #end #local Ind2 = Ind2+1; #end
Ce que nous faisons est de passer en revue toutes les sphères (nous sautons la sphère courante bien que ce ne soit pas nécessaire, mais une petite optimisation est toujours une petite optimisation), de prendre le point d'intersection comme point de départ et la direction de la lumière comme vecteur de direction, et voir si le test d'intersection retourne une valeur positive pour l'une d'elles (et nous quittons immédiatement la boucle, puisqu'il n'est pas nécessaire de vérifier le reste).
Le résultat du test d'ombre est placé dans l'identifiant Shadowed comme une valeur booléenne (true si le point est ombré).
Le composant de diffusion de la lumière est généré quand un rayon de lumière touche une surface et est réfléchi également dans toutes les directions. La partie la plus brillante de la surface est celle dont le vecteur de la normale pointe directement dans la direction de la lumière. L'illumination diminue en relation avec le cosinus de l'angle entre le vecteur de la normale et le vecteur de la lumière.
#if(!Shadowed) // Diffuse: #local Factor = vdot(Normal, L); #if(Factor > 0) #local Pixel = Pixel + LVect[Ind][1]*Coord[closest][2]*Factor; #end
Le code de la lumière diffuse est surprenant par sa petite taille.
Il y a un très joli tour en mathématiques pour avoir le cosinus d'un angle entre deux vecteurs unitaires : c'est leur produit matriciel.
Ce que nous devons faire est le calcul du produit matriciel du vecteur de la normale et du vecteur de la lumière (les deux ont été préalablement normalisés). Si ce produit est négatif, alors le vecteur de la normale pointe dans une direction opposée au vecteur de la lumière. Donc nous ne nous intéressons qu'aux valeurs positives.
Ensuite, nous ajoutons à la couleur du pixel la couleur de la source de lumière multipliée par la couleur de la surface de la sphère multipliée par le produit matriciel. Cela nous donne le composant de diffusion de l'illumination.
Le composant spéculaire de l'illumination vient du fait que beaucoup de surfaces ne reflètent pas la lumière également dans toutes les directions, mais elles reflètent plus de lumière dans la direction du "rayon réfléchi", donc, la surface a quelques propriétés d'un miroir. La partie la plus brillante de la surface est celle où le rayon réfléchi pointe dans la direction de la lumière.
L'illumination photoréaliste est très difficile à obtenir et il y a beaucoup de modèles d'illumination différents, qui tentent de simuler l'illumination du monde réel avec plus ou moins de précision. pour notre simple raytraceur nous utilisons juste un modèle d'illumination Phong, qui est plus que suffisant.
// Spéculaire : #local Factor = vdot(vnormalize(Refl), L); #if(Factor > 0) #local Pixel = Pixel + LVect[Ind][1]* pow(Factor, Coord[closest][3].x)* Coord[closest][3].y; #end
Le calcul est similaire à l'illumination diffuse avec les différences suivantes :
Phong très simple).Ainsi, la couleur que nous ajoutons à celle du pixel est la couleur de la source de lumière multipliée par le produit matriciel (qui est élevé à la puissance donnée) et par la quantité d'éclaircissement donnée.
Puis nous fermons le bloc de code :
#end // if(!Shadowed) #local Ind = Ind+1; #end // while(Ind < LightAmnt)
// Réflexion: #if(recLev < MaxRecLev & Coord[closest][1].y > 0) #local Pixel = Pixel + Trace(IP, Refl, recLev+1)*Coord[closest][1].y; #end
Un autre bel aspect du traçage est que la réflexion est facile à calculer.
Ici nous vérifions que le niveau de récursion n'a pas atteint la limite et que la sphère a un composant de réflexion défini. Si les deux sont vrais, nous ajoutons le composant réfléchi (la couleur du rayon réfléchi multiplié par le facteur de réflexion) à la couleur du pixel.
C'est ici qu'intervient l'appel récursif (la macro s'appelle elle-même). Le niveau de récursion (recLev) est augmenté de un pour l'appel suivant pour que quelque part plus bas, la série d'appels de Trace() sache s'arrêter (évitant au rayon de repartir en arrière et à rebondir indéfiniment entre deux miroirs). C'est en gros ainsi que fonctionne le paramètre global max_trace_level dans POV-Ray.
Enfin, nous fermons le bloc de code et renvoyons la couleur du pixel depuis la macro :
#end // else Pixel #end
#debug "Rendering...\n\n" #declare Image = array[ImageWidth][ImageHeight] #declare IndY = 0; #while(IndY < ImageHeight) #declare CoordY = IndY/(ImageHeight-1)*2-1; #declare IndX = 0; #while(IndX < ImageWidth) #declare CoordX = (IndX/(ImageWidth-1)-.5)*2*ImageWidth/ImageHeight; #declare Image[IndX][IndY] = Trace(-z*3, <CoordX, CoordY, 3>, 1); #declare IndX = IndX+1; #end #declare IndY = IndY+1; #debug concat("\rDone ", str(100*IndY/ImageHeight, 0, 1), "% (line ", str(IndY, 0, 0), " out of ", str(ImageHeight, 0, 0), ")") #end #debug "\n"
Maintenant, nous avons juste à calculer l'image dans le tableau de couleurs. Ce tableau est défini au début de ce code; c'est un tableau à deux dimensions représentant l'image finale que nous avons calculée.
Notez comment nous utilisons le flux #debug pour sortir l'information utile sur le processus de rendu pendant le calcul. Cela est beau parce que le processus de traçage est relativement lent et il est bon de donner à l'utilisateur un retour sur ce qui se passe et le temps que ça prend. (notez aussi que le caractère "%" dans la chaîne de la seconde commande #debug fonctionnera correctement seulement dans la version Windows de POV-Ray; pour les autres versions il peut être nécessaire de la convertir en "%%".)
Ce que nous faisons ici est de passer sur tous les "pixels" de l'"image" (c.a.d. le tableau) et de calculer la position de la caméra pour chacun d'eux (fixée à -z*3 ici) et la direction du rayon qui passe à travers le pixel (dans ce code, le "plan visuel" est fixé et positionné dans le plan x-y et sa hauteur est fixée à 1).
Ce que fait la ligne suivante :
#declare CoordY = IndY/(ImageHeight-1)*2-1;
est le dimensionnement de IndY pour qu'il aille de -1 à 1. Il est d'abord divisé par la valeur maximale (qui est ImageHeight-1) puis multiplié par deux et soustrait de 1. Cela donne une valeur allant de -1 à 1.
Le CoordX est calculé de la même manière, mais il est aussi multiplié par le ratio d'aspect de l'image que nous calculons (ainsi nous n'aurons pas une image déformée).
Si vous pensez que ce que vous avez vu est avancé, alors vous n'avez rien vu. Maintenant vient le noyau dur du code avancé de POV-Ray, aussi soyez préparés. Cela pourrait être appelé La section réellement avancée.
Nous avons maintenant calculé l'image dans le tableau de couleurs. Toutefois, nous devons encore montrer ces "pixels" colorés à l'écran, donc, nous devons faire en sorte que POV-Ray rende nos pixels pour créer une vraie image.
Il y a plusieurs façons de le faire, toutes sont plus ou moins "bâtardes" (puisqu'il n'y a aucun moyen pour créer directement une application d'image depuis un groupe de couleurs). On peut créer des boîtes colorées représentant chaque pixel, ou on peut sortir une image en fichier ascii (principalement PPM) puis le lire comme une application d'image. La première a le désavantage de demander une énorme quantité de mémoire et de perdre les interpolations bilinéaires de l'image; la seconde a le désavantage de demander un fichier temporaire.
Ce que nous allons faire est de calculer un mesh2 coloré qui représente l'"écran".
Comme les couleurs sont interpolées entre les angles d'un triangle, l'interpolation bilinéaire vient avec.
Bien que tous les triangles soient dans le plan x-y et qu'ils ont tous la même taille, la structure du maillage est plutôt compliquée.
L'image suivante montre comment les triangles sont arrangés sur une image de 4x3 pixels :

Les paires de nombres entre parenthèses représentent les coordonnées du pixel de l'image (ex. (0, 0) se réfère au pixel au coin gauche bas de l'image et (3, 2) au pixel en haut à droite). Donc, les triangles seront colorés comme des pixels d'image en ces points. Les couleurs seront ensuite interpolées entre elles sur la surface des triangles.
Les cercles creux et plein de l'image représentent les points de sommet des triangles, et les lignes les connectant montrent comment les triangles sont arrangés. Les plus petits nombres près des cercles indiquent leur valeur d'index (celui qui sera créé dans le mesh2).
Nous notons deux choses qui peuvent sembler bizarres : premièrement il y a des sommets supplémentaires en dehors du maillage, et deuxièmement, il y a des sommets supplémentaires au milieu de chaque carré.
Commençons par les sommets au milieu des carrés : nous pouvons seulement faire chaque carré avec deux triangles au lieu de quatre, comme là. toutefois, l'interpolation de couleur n'est pas belle dans ce cas là, car une diagonale nette apparaît là où les deux triangles se touchent. Si nous faisons chaque carré avec quatre triangles, alors les lignes diagonales sont moins apparentes, et l'interpolation se rapproche plus d'une interpolation bilinéaire. Et qu'elle est la couleur des points du milieu ? Bien sûr c'est la moyenne des couleurs des quatre points dans les coins.
Deuxièmement : oui, les points de sommet supplémentaires en dehors du maillage sont complètement obsolètes et ne prennent pas part à la création du maillage. Nous pouvons parfaitement créer le même maillage sans eux. Toutefois, se débarrasser de ces points de sommet supplémentaires rend notre vie plus difficile lors de la création des triangles, car cela rend l'indexation des points ardue. Il peut ne pas être très laborieux de s'en passer, mais ils ne consomment pas beaucoup de ressources et ils nous facilitent la vie, aussi laissons-les vivre (si vous voulez les enlever, allez-y).
Cela signifie que pour chaque pixel nous créons deux points de sommet, un à la position du pixel et un déplacé de "0.5" dans les directions x et y. Puis nous spécifions la couleur de chacun d'eux: pour le premier c'est directement la couleur du pixel correspondant; pour les autres, c'est la moyenne des quatre pixels voisins.
Examinons la création du maillage étape par étape :
#default {finish {ambient 1}} #debug "Creating colored mesh to show image...\n" mesh2 { vertex_vectors { ImageWidth*ImageHeight*2, #declare IndY = 0; #while(IndY < ImageHeight) #declare IndX = 0; #while(IndX < ImageWidth) <(IndX/(ImageWidth-1)-.5)*ImageWidth/ImageHeight*2, IndY/(ImageHeight-1)*2-1, 0>, <((IndX+.5)/(ImageWidth-1)-.5)*ImageWidth/ImageHeight*2, (IndY+.5)/(ImageHeight-1)*2-1, 0> #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end }
Avant tout, nous utilisons une belle astuce dans POV-Ray : puisque nous n'utilisons pas de source de lumière et qu'il n'y a rien pour illuminer notre maillage, nous mettons la valeur ambient du maillage à 1. Nous faisons cela en le rendant simplement à défaut avec la commande #default, aussi nous n'avons plus à nous en occuper.
Comme nous l'avons vu avant, nous faisons cela pour créer deux points de sommet pour chaque pixel. Ainsi nous connaissons le nombre de vecteurs de sommet qu'il y aura : ImageWidth*ImageHeight*2
C'est la partie facile; maintenant nous devons imaginer la manière de créer les points de sommet eux-mêmes. Chaque position de sommet doit correspondre à la position du pixel qu'il représente, aussi nous passons sur tous les index de pixel (pratiquement les paires de nombres entre parenthèses dans l'image du dessus) et créons les points de sommet en utilisant ces valeurs d'index. La position de ces pixels et sommets sont les mêmes comme prévu lors du calcul de l'image elle-même (dans la partie précédente). Donc la coordonnée y de chaque point de sommet va de -1 à 1 et de même pour la coordonnée x, mais dimensionnée avec le ratio d'aspect.
Si nous regardons la création du premier vecteur dans le code au-dessus, nous verrons qu'il est identique au vecteur de direction calculé lors de la création de l'image.
Le second vecteur doit être déplacé de 0.5 dans les deux directions, et c'est exactement ce qui est fait là. La définition du second vecteur est identique à celle du premier sauf que les valeurs d'index sont déplacées de 0.5. Cela crée les points au centre des carrés.
Les valeurs d'index de ces points seront arrangées comme montré dans l'image.
texture_list { ImageWidth*ImageHeight*2, #declare IndY = 0; #while(IndY < ImageHeight) #declare IndX = 0; #while(IndX < ImageWidth) texture {pigment {rgb Image[IndX][IndY]}} #if(IndX < ImageWidth-1 & IndY < ImageHeight-1) texture {pigment {rgb (Image[IndX][IndY]+Image[IndX+1][IndY]+ Image[IndX][IndY+1]+Image[IndX+1][IndY+1])/4}} #else texture {pigment {rgb 0}} #end #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end }
La création des textures est trés similaire à la création des points de sommet (nous pourrions faire les deux dans la même boucle, mais à cause de la syntaxe de mesh2 nous devons les faire séparemment).
Donc ce que nous devons faire est de passer sur tous les pixels de l'image et de créer les textures pour chacun d'eux. La première texture est la couleur du pixel lui-même. La seconde texture est la moyenne des quatre pixels voisins.
Note : nous ne pouvons les calculer que pour les points de sommet au milieu des carrés; pour les points surnuméraires en dehors de l'image nous définissons seulement une texture noire.
Les textures ont les mêmes valeurs d'index que les points de sommet.
Ceci est plus difficile. En fait nous devons créer quatre triangles pour chaque "carré" entre les pixels. Combien cela fait-il de triangles ?
Voyons la boucle de création d'abord :
face_indices { (ImageWidth-1)*(ImageHeight-1)*4, #declare IndY = 0; #while(IndY < ImageHeight-1) #declare IndX = 0; #while(IndX < ImageWidth-1) ... #declare IndX = IndX+1; #end #declare IndY = IndY+1; #end }
Le nombre de "carrés" est celui des pixels dans chaque direction moins un. Donc, le nombre de carrés dans la direction x sera celui des pixels dans la direction x moins un. La même chose pour la direction y. Comme nous voulons quatre triangles pour chaque carré, le nombre total de triangles sera (ImageWidth-1)*(ImageHeight-1)*4.
Donc pour créer chaque carré nous bouclons autant de fois qu'il y a de pixels pour chaque direction moins un.
Maintenant, au coeur de chaque boucle, nous devons créer les quatre triangles. Examinons le premier :
<IndX*2+ IndY *(ImageWidth*2), IndX*2+2+IndY *(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ IndY *(ImageWidth*2), IndX*2+2+IndY *(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2),
Cela crée un triangle avec une texture pour chaque sommet. Les trois premières valeurs (les indices pour les points de sommet) sont identiques aux trois suivantes (les indices des textures) parce que les valeurs d'index sont exactement les mêmes pour les deux.
Le IndX est toujours multiplié par 2 parce que nous avons deux points de sommet pour chaque pixel et IndX suit les pixels. De même, IndY est toujours multiplié par ImageWidth*2 parce que c'est toujours la longueur d'une ligne d'index de points (c.a.d. pour aller d'une ligne à l'autre à la même coordonnée x, nous devons avancer de ImageWidth*2 dans les valeurs d'index).
Ces deux choses sont identiques pour tous les triangles. Qu'est-ce qui décide que le point de sommet choisi est le "+1" ou "+2" (ou "+0" quand il n'y a rien). Pour IndX "+0" est le pixel courant, "+1" choisit le point au centre du carré et "+2" choisit le pixel suivant. Pour IndY "+1" choisit la ligne suivante de pixels.
Ainsi cette définition de triangle crée un triangle utilisant le point de sommet du pixel courant, celui du pixel suivant et celui au centre du carré.
La définition du triangle suivant est comme ceci :
<IndX*2+ IndY *(ImageWidth*2), IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ IndY *(ImageWidth*2), IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2),
Celle-ci définit le triangle utilisant le point actuel, le point dans la ligne suivante et le point au milieu du carré.
Les deux définitions suivantes définissent les deux autres triangles :
<IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+ (IndY+1)*(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2), <IndX*2+2+IndY *(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)>, IndX*2+2+IndY *(ImageWidth*2), IndX*2+2+(IndY+1)*(ImageWidth*2), IndX*2+1+IndY *(ImageWidth*2)
La dernière chose restante est la définition de la caméra, pour que POV-Ray puisse calculer correctement l'image :
camera {orthographic location -z*2 look_at 0}
Pourquoi "2" ? Comme le vecteur de direction par défaut est <0, 0, 1> et que le vecteur up par défaut est <0, 1, 0> et que nous voulons que la direction vers le haut couvre deux unités, nous devons déplacer la caméra de deux unités.
| 2.4 Questions et trucs |