4

I wrote a program that takes in entry some points, expressed in 3D coordinates and that must be drawn in a 2D canvas. I use perspective projection, homogeneous coordinates and similar triangles to do that. However, my program does not work and I actually don't know why.

I followed two tutorials. I really understood the geometrical definitions and properties I have read. However, my implementation fails... I will write references to these both courses little by little, to make your reading more confortable :).

Overview : geometrical reminders

The perspective projection is done following this workflow (cf. these 2 courses - I wrote pertinent links about the latter (HTML anchors) further down, in this post) :

  1. Definition of the points to draw, expressed according to the world's coordinates system ; Definition of the matrix of projection, which is a matrix of transformation that "converts" a point expressed according to the world coordinates system into a point expressed according to the camera's coordinates system (NB : I believe this matrix can be understood as being the 3D object "camera")

  2. Product of these points with this matrix (as defined in the adequat part, below, in this document) : the product of these world-expressed points results in the conversion of these points to the camera's coordinates system. Note that points and matrix are expressed in 4D (concept of homogenous coordinates).

  3. Use of similar triangles concept to project (only computing is done at this step) on the canvas the in-camera-expressed points (using their 4D coordinates). After this operation, the points are now expressed in 3D (the third coordinate is computed but not actually used on the canvas). The 4th coordinate is removed because not useful. Note that the 3rd coordinate won't be useful, except to handle z-fighting (though, I don't want to do that).

  4. Last step : rasterization, to actually draw the pixels on the canvas (other computing AND displaying are done at this step).

First, the problem

Well, I want to draw a cube but the perspective doesn't work. The projected points seem to be drawn withtout perspective.

What result I should expect for

The result I'm expecting is the cube displayed in "Image" part of this below PNG :

The result I'm expecting is the cube displayed in "Image" part of this PNG

What I'm outputting

The faces of my cube are odd, as if perspective wasn't well used.

The faces of my cube are odd, as if perspective wasn't well used.

I guess I know why I'm having this problem...

I think my projection matrix (i.e. : the camera) doesn't have the good coefficients. I'm using a very simple projection matrix, without the concepts of fov, near and far clipping planes (as you can see belower).

Indeed, to get the expected result (as previouslyt defined), the camera should be placed, if I'm not mistaken, at the center (on axes x and y) of the cube expressed according to the world coordinate system and at the center (on axes x and y) of the canvas, which is (I make this assumption) placed 1 z in front of the camera.

The Scastie (snippet)

NB : since X11 is not activated on Scastie, the window I want to create won't be shown.

https://scastie.scala-lang.org/N95TE2nHTgSlqCxRHwYnxA

Entries

Perhaps the problem is bound to the entries ? Well, I give you them.

Cube's points

Ref. : myself

val world_cube_points : Seq[Seq[Double]] = Seq(
  Seq(100, 300, -4, 1), // top left
  Seq(100, 300, -1, 1), // top left z+1
  Seq(100, 0, -4, 1), // bottom left
  Seq(100, 0, -1, 1), // bottom left z+1
  Seq(400, 300, -4, 1), // top right
  Seq(400, 300, -1, 1), // top right z+1
  Seq(400, 0, -4, 1), // bottom right
  Seq(400, 0, -1, 1) // bottom right z+1
)

Transformation (Projection) matrix

Ref. : https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix , End of the Part. "A Simple Perspective Matrix"

Note that I'm using the simplest perspective projection matrix : I don't use concept of fov, near and far clipping planes.

new Matrix(Seq(
  Seq(1, 0, 0, 0),
  Seq(0, 1, 0, 0),
  Seq(0, 0, -1, 0),
  Seq(0, 0, -1, 0)
))

Consequence of this matrix : each point P(x;y;z;w) producted with this matrix will be : P'(x;y;-z;-z).

Second, the first operation my program does : a simple product of a point with a matrix.

Ref. : https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection#homogeneous-coordinates

/**
  * Matrix in the shape of (use of homogeneous coordinates) :
  * c00 c01 c02 c03
  * c10 c11 c12 c13
  * c20 c21 c22 c23
  *   0   0   0   1
  *
  * @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 the matrix.
    *
    * @param point a point P(x ; y ; z ; 1)
    * @return a new point P'(
    *         x * c00 + y * c10 + z * c20
    *         ;
    *         x * c01 + y * c11 + z * c21
    *         ;
    *         x * c02 + y * c12 + z * c22
    *         ;
    *         1
    *         )
    */
  def product(point : Seq[Double]) : Seq[Double] = {
    (0 to 3).map(
      i => content(i).zip(point).map(couple2 => couple2._1 * couple2._2).sum
    )
  }

}

Then, use of similar triangles

Ref. 1/2 : Part. "Of the Importance of Converting Points to Camera Space " of https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points

Ref. 2/2 : https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection#time-to-work-in-full-3d

NB : at this step, the entries are points expressed according to the camera (i.e. : they are the result of the precedently defined product with the precedently defined matrix).

class Projector {

  /**
    * Computes the coordinates of the projection of the point P 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 before using this function.
    * @return the point P', projection of P.
    */
  def drawPointsOnCanvas(points : Seq[Seq[Double]]) : Seq[Seq[Double]] = {
    points.map(point => {
      point.map(coordinate => {
        coordinate / point(3)
      }).dropRight(1)
    })

  }

}

Finally, the drawing of the projected points, onto the canvas.

Ref. : Part. "From Screen Space to Raster Space" of https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points

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 = 820
  val CANVAS_HEIGHT = 820
  val IMAGE_WIDTH = 900
  val IMAGE_HEIGHT = 900

  def display = {
    setTitle("Perlin")
    setSize(IMAGE_WIDTH, IMAGE_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.fillRect((normalized_drawn_point.head * IMAGE_WIDTH).toInt, ((1 - normalized_drawn_point(1)) * IMAGE_HEIGHT).toInt, 5, 5)

        graphics.drawString(
          "P(" + (normalized_drawn_point.head * IMAGE_WIDTH).toInt + " ; "
          + ((1 - normalized_drawn_point(1)) * IMAGE_HEIGHT).toInt + ")",
          (normalized_drawn_point.head * IMAGE_WIDTH).toInt - 50, ((1 - normalized_drawn_point(1)) * IMAGE_HEIGHT).toInt - 10
        )
      }
    })
  }

}

Question

What's wrong with my program ? I understood the geometrical concepts explained by these both tutorials that I read carefully. I'm pretty sure my product works. I think either the rasterization, or the entries (the matrix) could be wrong...

JarsOfJam-Scheduler
  • 2,809
  • 3
  • 31
  • 70
  • Just to make sure that I get youright: Your problem is, that the resulting (the projected) coordinates are wrong. The points (drawn_points) which you pass to your Canvas are already transformed - is that right? – TobiSH Mar 03 '18 at 19:55
  • Duplicate: [My unworking implementation of perspective projection](https://stackoverflow.com/questions/49018456/my-unworking-implementation-of-perspective-projection) – Andrey Tyukin Mar 03 '18 at 23:06
  • @AndreyTyukin This is not a dupplicate : indeed I re-used the (almost !) same format for my both questions. However, the problem is completly different. – JarsOfJam-Scheduler Mar 04 '18 at 09:27
  • @TobiSH "Your problem is, that the resulting (the projected) coordinates are wrong" that's it, you are right. "The points (drawn_points) which you pass to your Canvas are already transformed - is that right?" : yes. First, they are transformed to be expressed according to the camera's coordinates system (initially, they are expressed according to the world coord. system). This is the aim of the matrix product. Second, `Projector::drawPointOnCanvas` uses similar triangles to project the points, expressed according to the camera's coordinates system, on the canvas. (Canvas just norm. and raster) – JarsOfJam-Scheduler Mar 04 '18 at 09:31

2 Answers2

1

Note that I'm using the simplest perspective projection matrix : I don't use concept of fov, near and far clipping planes.

I think that your projection matrix is too simple. By dropping the near and far clipping planes, you are dropping perspective projection entirely.

You do not have to perform the z-clipping step, but you need to define a view frustum to get perspective to work. I believe that your projection matrix defines a cubic "view frustrum", hence no perspective.

See http://www.songho.ca/opengl/gl_projectionmatrix.html for a discussion of how the projection matrix works.

Rich
  • 15,048
  • 2
  • 66
  • 119
  • I think you're wrong. Indeed, a simple projection matrix is also used in this course : https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection – JarsOfJam-Scheduler Mar 06 '18 at 18:36
  • Don't you rather think that my problem is due to the fact I didn't normalize the value of the z coordinate in the `projectionMatrix * point` product, before using the similar triangles' operation, as explained in Part "Remapping the Z-coordinate", in https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix ? If yes : I don't know why this remapping is required ? – JarsOfJam-Scheduler Mar 06 '18 at 18:36
  • Ok, perhaps I'm wrong. I would suggest trying both approaches and seeing which works. I don't have time to try this for you at the moment. I'll leave my answer up for now pending your findings, but I will delete it if it is wrong. Sorry! – Rich Mar 07 '18 at 10:51
1

Quoting the Scratchapixel page:

... If we substitute these numbers in the above equation, we get:

enter image description here

Where y' is the y coordinate of P'. Thus:

enter image description here

This is probably one the simplest and most fundamental relation in computer graphics, known as the z or perspective divide. The exact same principle applies to the x coordinate. ...

And in your code:

def drawPointsOnCanvas(points : Seq[Seq[Double]]) : Seq[Seq[Double]] = {
    points.map(point => {
      point.map(coordinate => {
        coordinate / point(3)
                     ^^^^^^^^
    ...

The (3) index is the 4th component of point, i.e. its W-coordinate, not its Z-coordinate. Perhaps you meant coordinate / point(2)?

meowgoesthedog
  • 14,670
  • 4
  • 27
  • 40
  • No, the 4th component of `point` is equalled to its 3rd one since the two last rows of my projection matrix are : `Seq(0, 0, -1, 0), Seq(0, 0, -1, 0)`. However I think I found the solution : ScratchaPixel, in another Web page of its site, indicates that a perspective projection matrix substitutes all these projection operations (similar triangle, among others). Perhaps I really should use a matrix that simply moves the camera (thus, not an actual persp. projection matrix). I will try it and update this question. – JarsOfJam-Scheduler Mar 11 '18 at 16:09
  • I personally do not prefer the projection matrix approach anyway, because i) too many conflicting conventions, and ii) makes near-plane clipping more difficult (will need to cull in world-space instead of camera-space). – meowgoesthedog Mar 11 '18 at 16:21
  • Well, in a first phase I won't use a perspective projection matrix. I indeed prefer to continue using explicitly the similar triangles, etc. I will use exactly the data of ScratchaPixel (i.e. : the same perspective projection matrix and the same coordinates of the points to draw in 2D from 3D). I will update this StackOverflow question when I'll have something new :) – JarsOfJam-Scheduler Mar 11 '18 at 17:14