Salut à tous,
Je reviens vers vous à propos du cours proposé par ScratchAPixel : https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points .
Quelques zones d’ombre subsistent encore malgré le fait que j’ai correctement implémenté la projection perspective qu’ils présentent.
Remarques générales
Aucune matrice de projection perspective et aucun frustrum de vue ne sont utilisés.
Résumé précis du cours
A. Ecrire une matrice qui définit l’objet 3D dans le monde :
1 2 3 4 5 6 7 8 9 10 11 | val world_cube_points : Seq[Seq[Double]] = Seq( Seq(-150, 150, -150), Seq(-150, 150, 150), Seq(150, 150, -150), Seq(150, 150, 150), Seq(-150, -150, -150), Seq(-150, -150, 150), Seq(150, -150, -150), Seq(150, -150, 150) ) |
B. Définir une matrice de transformation world-to-camera et multiplier chaque point de l’objet par cette matrice, afin de changer l’origine de ce dernier en remplaçant l’origine du monde par l’origine de world-to-camera :
B.1. Matrice world-to-camera (une simple translation) :
Il faut se souvenir d’une convention qui implique que l’axe Z de la caméra ait la même direction que l’axe Z négatif du monde. D’où le -1
dans la matrice (une coordonnée z positive dans le monde sera négative dans la caméra). Seuls les points (définis dans le système de coordonnées de la caméra) avec un z négatif seront visibles.
1 2 3 4 5 6 | val translation_matrix_world_to_camera : Matrix = new Matrix(Seq( Seq(1, 0, 0, 0), Seq(0, 1, 0, 0), Seq(0, 0, -1, 0), Seq(1, 1, -400, 1) )) |
B.2. Opération de produit et appels à ce produit (la matrice est lue ligne par ligne, et non colonne par colonne, et comme me l’a fait remarquer@JuDePom, je ne respecte pas du coup les conventions des math/physiciens, ce qui peut donc vous perturber un petit peu à la lecture - je ferai une nouvelle version c par c plus tard)
1 2 3 4 5 6 7 8 9 | def product(point : Seq[Double]) : Seq[Double] = (0 to 2).map(i => (0 to 3).map(i2 => { var coefficient : Double = 0 if(i2 == point.length) { coefficient = 1 } else { coefficient = point(i2) } content(i2)(i) * coefficient }).sum) |
Et les appels qui vont avec :
1 2 3 | world_cube_points.map(point => { translation_matrix_world_to_camera.product(point) }) |
C. On projette les points sur le plan image (c’est un plan, du coup de taille infinie) qui contient le canvas (plan qui est perpendiculaire à l’axe Z de la caméra et qui est situé 1 unité devant) ; cette projection se fait en utilisant la notion des triangles similaires : . A partir du fait que BC=B’C’ (1 propriété des triangles similaires), on en déduit les coordonnées du point projeté : PointProjeté(point_camera.X/point_camera.Z ; point_camera.Y/point_camera.Z ; 1)
. En conséquence de la puce N°B.1. (seuls les points visibles sont ceux qui ont un z négatif dans la caméra), on constate l’émergence d’un problème : PointProjeté.X
et PointProjeté.Y
auront chacun un signe contraire à celui de PointCaméra
, puisque PointCaméra.Z
sera négatif : le point projeté irait donc à droite alors que dans la caméra (et dans le monde), il va à gauche… Dès lors, pour corriger ce problème, les coordonnées du point projeté deviennent : PointProjeté(point_camera.X/-point_camera.Z ; point_camera.Y/-point_camera.Z ; 1)
.
C.1. La fonction de projection
1 2 3 4 5 6 7 | def projectPointsOnImageView(points : Seq[Seq[Double]]) : Seq[Seq[Double]] = { points.map(point => { point.map(coordinate => { coordinate / -point(2) }) }) } |
C.2. L’appel :
1 2 3 | val points_to_draw_on_canvas = projector.projectPointsOnImageView(world_cube_points.map(point => { translation_matrix_world_to_camera.product(point) })) |
D. Normaliser pour rasterizer Rendus à cette étapes, on constate que les points ont été projetés de la caméra sur le plan image.
Il s’agit donc, dans cette dernière étape, de les dessiner sur le canvas pour les montrer à l’utilisateur (donc exprimer les coordonnées en pixels, i.e. : rasterizer).
D.1. Normalisation et Rasterization L’idée c’est encore d’effectuer des translations pour passer de système de coordonnées en système de coordonnées, bref.
1 2 3 | val normalized_drawn_point = Seq((point.head + CANVAS_WIDTH * 0.5) / CANVAS_WIDTH, (point(1) + CANVAS_HEIGHT * 0.5) / CANVAS_HEIGHT) // Normalization graphics.fillRect((normalized_drawn_point.head * IMAGE_WIDTH).toInt, ((1 - normalized_drawn_point(1)) * IMAGE_HEIGHT).toInt, 5, 5) // Rasterization : drawing the pixel (pixel's dimensions : 5x5, to make it visible) |
L’appel :
new Canvas(points_to_draw_on_canvas).display
Mes questions
Bon maintenant que j’ai résumé les explications de ScratchAPixel (ABREV. : "SAP"), je vais pouvoir vous poser les questions que j’ai toujours en tête à l’issue de mes nombreuses relectures du cours
!!! ABREV de ScratchAPixel : SAP !!!
Q.1. L’objet 3D est-il réellement défini dans le repère monde ?
SAP indique que oui. Or, sur un autre cours, j’ai trouvé la notion de "repère modèle". Tout objet 3D se définit d’abord dans son repère modèle, PUIS si on veut établir des relations d’espace entre eux, on doit les regrouper dans un même repère, nommé le "repère monde".
Or SAP indique que typiquement, mon objet 3D :
1 2 3 4 5 6 7 8 9 10 11 | val world_cube_points : Seq[Seq[Double]] = Seq( Seq(-150, 150, -150), Seq(-150, 150, 150), Seq(150, 150, -150), Seq(150, 150, 150), Seq(-150, -150, -150), Seq(-150, -150, 150), Seq(150, -150, -150), Seq(150, -150, 150) ) |
… est bien défini dans un repère monde et, en outre, SAP ne parle jamais de repère modèle.
Est-il sous-entendu que ce repère modèle est aussi le repère monde, car un seul objet est présent dans la scène ?
Est-il réellement important d’utiliser le repère caméra dans cette implémentation-là ?
Le seul argument donné par SAP est que l’utilisation des triangles similaires pour aboutir à la trouvaille des coordonnées du point projeté nécessite le repère de la caméra.
C’est selon moi un mauvais argument, car on aurait tout aussi bien pu, d’après moi j’insiste, utiliser l’origine du repère monde comme extrémité de la ligne de vue, puis appliquer tranquillement, sans souci, les triangles similaires, etc. En effet, le repère monde est un repère comme un autre, et notamment comme celui de la caméra…
Ainsi, en quoi le repère de la caméra est-il réellement nécessaire ? Je rappelle qu’il n’y a même pas de frustrum de vue, du coup il n’y a aucune restriction quant à la visibilité des points de la scène 3D projetés en 2D. Enfin si, il y a bien UNE restriction de visibilité : la taille du canvas mais rien à voir avec la caméra…
SAP indique que la direction de l’axe Z de la caméra est la même que celle de l’axe Z négatif du monde, pourtant c’est faux
SAP explique que la seule façon d’avoir un mapping entre l’axe X de la caméra et l’axe X du monde et l’axe Y de la caméra et l’axe Y du monde est d’avoir une direction Z de la caméra = à la direction Z du monde.
OR C’EST FAUX, puisque si on effectue une rotation sur l’axe X de la caméra pour aligner son axe Z avec celui négatif du monde, on fait pointer l’axe Y de la caméra dans la direction opposée à l’axe Y du monde. Et à peu près pareil pour une rotation sur l’axe X de la caméra.
L’axe Z de la caméra pointe donc bel et bien sur l’axe Z positif du monde. PAR CONTRE, SAP multiplie par -1
la coordonnée z de chaque point du monde en train d’être transformé pour être placé dans la caméra. Mais en soi, j’insiste : "L’axe Z de la caméra pointe donc bel et bien sur l’axe Z positif du monde".
Je me trompe ?