NB : je m’adresse avant-tout à des informaticiens qui sont habitués aux matrices et qui ont déjà bien étudié la projection en perspective avec les triangles similaires, bien que j’en ré-explique le fonctionnement.
Salut à tous !
J’essaie de mettre en place un petit programme qui affiche des points initialement dans une scène 3D dans une image 2D. Pour ce faire, j’utilise la projection perspective avec les triangles similaires.
Mon programme est entièrement écrit, mais les points apparaissent toujours au même endroit et je ne sais pas pourquoi.
Présentation du topic
-
Dans un premier temps, je vais vous décrire dans les grandes lignes le fonctionnement de mon programme ;
-
Ensuite, je vais vous donner ses entrées ;
-
Puis sa sortie ;
-
Enfin, les sources.
Dans les grandes lignes…
Je souhaite donc dessiner des points initialement présents dans une scène 3D dans une image 2D.
Mon programme prend comme entrée les coordonnées $(x;y;z)$ de chacun de ces points, exprimés dans le système de coordonnées de cette scène 3D ("du monde") (fréquemment appelé "world coordinates system").
Puis il exprime ces coordonnées dans le système de coordonnées de la caméra ("camera coordinates system"). Pour ce faire, je donne en entrée à mon programme la matrice "world to camera", puis je multiplie chacun des points par cette matrice. Le produit de cette matrice avec un point donné est : $P_{camera}.X = P_{world}.X * M_{00} + P_{world}.Y * M_{10} + P_{world} * M_{20} + M_{30}$ avec $M$ la matrice "world to camera". Les lignes de cette matrice correspondent respectivement (de haut en bas) à l’axe $X$, l’axe $Y$, l’axe $Z$ et à la translation du système de coordonnées de la caméra par rapport au système de coordonnées de la scène 3D ("du monde").
Ici, j’ai donc exprimé mes points dans le système de coordonnées de la caméra. Il me reste à projeter ces points du système de coordonnées de la caméra sur mon canvas ("mon image"). Pour ce faire, j’utilise la notion de triangles similaires : , $P_{projeté}.X = P_{camera}.X / P_{camera}.Z$ (le canvas se situe 1 unité devant, sur l’axe $Z$ donc, la caméra). On notera que, comme par convention le système de coordonnées de la caméra possède un axe Z dont la direction est à l’opposé de la direction de l’axe Z du système de coordonnées du monde ("de la scène 3D"), la conversion "world to camera" a donné un point exprimé dans la caméra tel que son $P_{camera}.Z$ est l’opposé du $P_{world}.Z$. Et seuls les points dont le $P_{camera}.Z$ du système de coordonnées de la caméra est négatif seront visibles. Or si tel est le cas, avec ma formule actuelle, si $P_{camera}.X$ est positif ça signifie que $P_{projeté}.X$ sera négatif… Il en est de même pour $$P_{projeté}.Y$ avec $P_{camera}.Y$. La solution pour corriger ce problème d’effet miroir est donc de modifier ma formule comme suit : $P_{projeté}.X = P_{camera}.X / -P_{camera}.Z$ (ajout du "$-$"). Au final ma formule est donc : $P_{projeté}.X = P_{camera}.X / -P_{camera}.Z$ et $P_{projeté}.Y = P_{camera}.Y / -P_{camera}.Z$.
Maintenant, mon point se situe bien sur mon canvas. Mais par définition, il est exprimé par rapport au système de coordonnées du canvas, dont l’origine est le centre du canvas. Je vais donc l’exprimer dans le système de coordonnées normalisé puis enfin, exprimer le point résultant dans le système de coordonnées de l’image ("rasterization"). $P_{normalized}.X = (P_{canvas}.X + WidthCanvas/2) / WidthCanvas$ et idem pour le $P_{normalized}.Y$. Enfin : $P_{image}.X = P_{normalized}.X \times ImageWidth$ et PAS idem pour le $P_{image}.Y$ : la formule correcte pour cette coordonnée est : $P_{image}.Y = (1 - P_{normalized}.Y) \times ImageHeight$ car l’axe $Y$ du système de coordonnées normalisé va vers le haut alors que le système de coordonnées rasterisé $Y$ va vers le bas, d’où le : $1 -$.
Et donc là, j’ai bel et bien projeté mon point sur le canvas.
Entrées de mon programme
Mes points, exprimés par rapport au système de coordonnées du monde ("scène 3D")
1 2 3 4 5 6 7 8 9 10 | val world_cube_points : Seq[Seq[Double]] = Seq( Seq(0, 1, 0), Seq(0, 1, 1), Seq(0, 0, 0), Seq(0, 0, 1), Seq(-1, 1, 0), Seq(-1, 1, 1), Seq(-1, 0, 0), Seq(-1, 0, 1) ) |
Matrice de projection (= système de coordonnées de la caméra, exprimé par rapport au système de coordonnées du monde)
NB : le format de cette matrice est différent de celui de la matrice $M$ présentée au début de ce message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | val matrix_world_to_camera : Matrix = new Matrix(Seq( /* [ x-axis-x y-axis-x z-axis-x translation-x x-axis-y y-axis-y z-axis-y translation-y x-axis-z y-axis-z z-axis-z translation-z 0 0 0 1 ] */ Seq(2, 4, -5, 4), Seq(5, 3, 1, 3), Seq(2, 2, -3, 4), Seq(0, 0, 0, 1) )) ` |
Sortie du programme
NB : remarquez, dans la fenêtre affichée, le petit point en haut à gauche. Il devrait y en avoir 8 (puisque j’affiche un cube).
Sources
Main.scala : définition du cube, de la matrice de la caméra, conversion des points 3D vers le système de la caméra, projection des points exprimés par rapport au système de la caméra sur le canvas, normalisation te rasterization
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | object Main { def main(args : Array[String]) : Unit = { val projector = new Projector() val world_cube_points : Seq[Seq[Double]] = Seq( Seq(0, 1, 0), Seq(0, 1, 1), Seq(0, 0, 0), Seq(0, 0, 1), Seq(-1, 1, 0), Seq(-1, 1, 1), Seq(-1, 0, 0), Seq(-1, 0, 1) ) val matrix_world_to_camera : Matrix = new Matrix(Seq( /* [ x-axis-x y-axis-x z-axis-x translation-x x-axis-y y-axis-y z-axis-y translation-y x-axis-z y-axis-z z-axis-z translation-z 0 0 0 1 ] */ Seq(2, 4, -5, 4), Seq(5, 3, 1, 3), Seq(2, 2, -3, 4), Seq(0, 0, 0, 1) )) val points_to_draw_on_canvas = projector.drawPointsOnCanvas(world_cube_points.map(point => { matrix_world_to_camera.product(point) })) new Canvas(points_to_draw_on_canvas).display } } ` |
Matrix.scala : produit d’une matrice avec un point de coordonnées $(x;y;z)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /** * Matrix in the shape of : * c00 c01 c02 c03 * c10 c11 c12 c13 * c20 c21 c22 c23 * c30 c31 c32 c33 * * @param content the content of the matrix */ class Matrix(val content : Seq[Seq[Double]]) { /** * Computes the product between a point P(x ; y ; z) and this matrix M. * * @param point a point P(x ; y ; z) * @return a new point P'( * c30 + x * c00 + y * c10 + z * c20 * ; * c31 + x * c01 + y * c11 + z * c21 * ; * c32 + x * c02 + y * c12 + z * c22 * ) */ def product(point : Seq[Double]) : Seq[Double] = { point.zipWithIndex.map( couple => content(couple._2).dropRight(1).zip(point).map(couple => couple._1 * couple._2).sum + content(couple._2).last ) } } ` |
Projector.scala : utilisation des triangles similaires pour imprimer sur le canevas les points exprimés dans la caméra
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Projector { /** * Computes the coordinates of the projection of the point P, projected on the canvas. * The canvas is assumed to be 1 unit forward the camera. * The computation uses the definition of the similar triangles. * * @param points the point P we want to project on the canvas. Its coordinates must be expressed in the coordinates * system of the camera. * @return the point P', projection of P. Its coordinates are expressed in the coordinates system of the camera. */ def drawPointsOnCanvas(points : Seq[Seq[Double]]) : Seq[Seq[Double]] = { points.map(point => { point.map(coordinate => { coordinate / -point(2) }).dropRight(1) }) } } |
Canvas.scala : normalisation et rasterization, dernière fonction appelée par Main.scala : à l’issue de cela, on obtient l’image finale avec, si tout marchait bien, mon cube…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import java.awt.Graphics import javax.swing.JFrame /** * Assumed to be 1 unit forward the camera. * Contains the drawn points. */ class Canvas(val drawn_points : Seq[Seq[Double]]) extends JFrame { val CANVAS_WIDTH = 200 val CANVAS_HEIGHT = 200 val IMAGE_WIDTH = 10 val IMAGE_HEIGHT = 10 def display = { setTitle("Perlin") setSize(CANVAS_WIDTH, CANVAS_HEIGHT) setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) setVisible(true) } override def paint(graphics : Graphics): Unit = { super.paint(graphics) drawn_points.foreach(point => { if(!(Math.abs(point.head) <= CANVAS_WIDTH / 2 || Math.abs(point(1)) <= CANVAS_HEIGHT / 2)) { println("WARNING : the point (" + point.head + " ; " + point(1) + ") can't be drawn in this canvas.") } else { val normalized_drawn_point = Seq((point.head + (CANVAS_WIDTH / 2)) / CANVAS_WIDTH, (point(1) + (CANVAS_HEIGHT / 2)) / CANVAS_HEIGHT) graphics.drawRect(normalized_drawn_point.head.toInt * IMAGE_WIDTH, (1 - normalized_drawn_point(1).toInt) * IMAGE_HEIGHT, 1, 1) } }) } } |
Question
Bon bein la question est assez simple : pourquoi ça marche pas ?
J’ai pourtant bien compris le fonctionnement d’une projection en perspective, les fondements géométriques quoi. Y a aucun souci là-dessus je pense… Visiblement je me suis peut-être trompé au niveau des valeurs de la matrice caméra ? Bref je suis un peu déconcerté . . .