我透视投影的不工作实现

时间:2018-02-27 21:46:15

标签: scala geometry projection perspective

我写了一个程序,它输入了一些点,以3D坐标表示,必须在2D画布中绘制。我使用透视投影,齐次坐标和类似的三角形来做到这一点。但是,我的程序不起作用,实际上我不知道为什么。

我遵循了两个教程。我真的了解我读过的几何定义和属性。但是,我的实现失败了......我会一点一点地写出对这两门课程的引用,以使你的阅读更加舒适:)。

概述:几何提醒

透视投影是在此工作流程之后完成的(参见这两个课程 - 我在本文中进一步编写了相关链接(HTML锚点)):

  1. 绘制点的定义,根据世界坐标系表示;投影矩阵的定义,它是一个变换矩阵,将根据世界坐标系表示的点“转换”为根据摄像机坐标系表示的点(NB:此矩阵也可以理解为摄像机)

  2. 带有此矩阵的这些点的乘积(如下面的适当部分所定义):这些点的乘积导致这些点转换为摄像机的坐标系。注意,点和矩阵用4D表示(同质坐标的概念)。

  3. 使用类似的三角形概念在画布上投影(仅在此步骤进行计算)相机内表达点(使用其4D坐标):它们现在以3D表示(第三个坐标是已计算但未在画布上实际使用

  4. 最后一步:光栅化,实际绘制画布上的像素(其他计算和显示在此步骤完成)。

  5. 首先,问题

    好吧,我想画一个立方体,但它没有出现。投影点似乎是在相同的坐标上绘制的。

    而不是我的立方体,只有一个黑色像素可见。

    enter image description here

    Scastie(片段)

    注意:由于Scastie上没有激活X11,我想创建的窗口将不会显示。

    https://scastie.scala-lang.org/2LQ1wSMBTWqQQ7hql35sOg

    条目

    问题可能与条目有关吗?好吧,我给你他们。

    立方体的分数

    参考。 :我自己

    val world_cube_points : Seq[Seq[Double]] = Seq(
      Seq(0, 40, 0, 1),
      Seq(0, 40, 10, 1),
      Seq(0, 0, 0, 1),
      Seq(0, 0, 10, 1),
      Seq(20, 40, 0, 1),
      Seq(20, 40, 10, 1),
      Seq(20, 0, 0, 1),
      Seq(20, 0, 10, 1)
    )
    

    转换(投影)矩阵

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

    val matrix_world_to_camera : Matrix = new Matrix(Seq(
      Seq(1, 0, 0, 0),
      Seq(0, 1, 0, 0),
      Seq(0, 0, 1, 0),
      Seq(0, 0, -1, 1)
    ))
    

    第二,我的程序执行的第一个操作:带矩阵的点的简单乘积。

    参考。 :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
        )
      }
    
    }
    

    然后,使用类似的三角形

    参考。 1/2:部分。 “将点转换为相机空间的重要性 “https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points

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

    注意:在此步骤中,条目是根据摄像机表示的点(即:它们是先前定义的产品的结果,具有先前定义的矩阵)。

    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)
        })
    
      }
    
    }
    

    最后,将投影点绘制到画布上。

    参考。 :部分。 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 = 60
      val CANVAS_HEIGHT = 60
      val IMAGE_WIDTH = 55
      val IMAGE_HEIGHT = 55
    
      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)
          }
        })
      }
    
    }
    

    ...和启动器

    object Main {
      def main(args : Array[String]) : Unit = {
        val projector = new Projector()
    
        val world_cube_points : Seq[Seq[Double]] = Seq(
          Seq(0, 40, 0, 1),
          Seq(0, 40, 10, 1),
          Seq(0, 0, 0, 1),
          Seq(0, 0, 10, 1),
          Seq(20, 40, 0, 1),
          Seq(20, 40, 10, 1),
          Seq(20, 0, 0, 1),
          Seq(20, 0, 10, 1)
        )
    
        val matrix_world_to_camera : Matrix = new Matrix(Seq(
          Seq(1, 0, 0, 0),
          Seq(0, 1, 0, 0),
          Seq(0, 0, 1, 0),
          Seq(0, 0, -1, 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
    
      }
    }
    

    问题

    我的计划有什么问题?我理解了这两个教程所解释的几何概念,我仔细阅读过。我很确定我的产品有效。我认为光栅化或条目(矩阵)可能是错误的......

1 个答案:

答案 0 :(得分:1)

您在规范化设备坐标上调用toInt(意味着有效范围为[0,1]):

normalized_drawn_point.head.toInt * IMAGE_WIDTH
                            ----- 

这会将其四舍五入为0或1,因此所有点都将位于屏幕的边框上。只有之后再乘以屏幕分辨率:

(normalized_drawn_point.head * IMAGE_WIDTH).toInt

(从技术上讲,如果屏幕坐标从零开始,它应该是* (IMAGE_WIDTH - 1),这很常见。垂直方向也是如此。)