Почему моя реализация перспективы не отображает грани моего куба?

Я написал программу, которая принимает некоторые точки, выраженные в трехмерных координатах, которые должны быть нарисованы на двухмерном холсте. Для этого я использую перспективную проекцию, однородные координаты и подобные треугольники. Однако моя программа не работает, и я даже не знаю почему.

Я следил за двумя уроками. Я действительно понял геометрические определения и свойства, которые я прочитал. Однако моя реализация терпит неудачу ... Я буду писать ссылки на оба этих курса понемногу, чтобы вам было удобнее читать :).

Обзор: геометрические напоминания

Перспективная проекция выполняется в соответствии с этим рабочим процессом (см. Эти 2 курса - я написал соответствующие ссылки о последних (привязки HTML) ниже, в этом посте):

  1. Определение точек для рисования, выраженных в мировой системе координат; Определение матрицы проекции, которая представляет собой матрицу преобразования, которая «преобразует» точку, выраженную в соответствии с мировой системой координат, в точку, выраженную в соответствии с системой координат камеры (примечание: я считаю, что эту матрицу можно понимать как трехмерную объект "камера")

  2. Произведение этих точек с этой матрицей (как определено в соответствующей части ниже в этом документе): произведение этих выраженных в мире точек приводит к преобразованию этих точек в систему координат камеры. Обратите внимание, что точки и матрица выражаются в 4D (концепция однородных координат).

  3. Использование аналогичной концепции треугольников для проецирования (на этом этапе выполняются только вычисления) на холсте точек, отображаемых в камере (с использованием их 4-мерных координат). После этой операции точки теперь выражаются в 3D (третья координата вычисляется, но фактически не используется на холсте). 4-я координата удалена, потому что она бесполезна. Обратите внимание, что третья координата не будет полезна, кроме как для обработки z-боя (хотя я не хочу этого делать).

  4. Последний шаг: растеризация, чтобы фактически нарисовать пиксели на холсте (на этом шаге выполняются другие вычисления и отображение).

Во-первых, проблема

Ну, я хочу нарисовать куб, но перспектива не работает. Спроецированные точки кажутся нарисованными без перспективы.

На какой результат мне стоит рассчитывать

Результат, которого я ожидаю, - это куб, отображаемый в части «Изображение» ниже PNG:

Ожидаемый результат - это куб, отображаемый в графической части этого PNG

То, что я вывожу

Грани моего куба странные, как будто перспектива использовалась неправильно.

Грани моего куба странные, как будто перспектива использовалась неправильно.

Думаю, я знаю, почему у меня такая проблема ...

Я думаю, что моя матрица проекции (например, камера) не имеет хороших коэффициентов. Я использую очень простую матрицу проекции без концепции fov, близкой к и дальние плоскости отсечения (как вы можете видеть ниже).

Действительно, чтобы получить ожидаемый результат (как определено ранее), камеру следует разместить, если я не ошибаюсь, в центре (по осям x и y). куба, выраженного в соответствии с мировой системой координат и в центре (по осям x и y) холста, который (я делаю это предположение) поместил 1 z перед камерой.

Скэсти (отрывок)

NB: поскольку X11 не активирован на Scastie, окно, которое я хочу создать, не будет отображаться.

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

Записи

Возможно проблема связана с записями? Что ж, даю вам их.

Очки куба

Ref. : сам

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
)

Матрица трансформации (проекции)

Ref. : https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix, конец части. «Простая матрица перспективы»

Обратите внимание, что я использую простейшую матрицу перспективной проекции: я не использую концепцию fov, ближней и дальней плоскостей отсечения.

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

Следствие этой матрицы: каждая точка P(x;y;z;w), созданная с помощью этой матрицы, будет: P'(x;y;-z;-z).

Во-вторых, первая операция, которую выполняет моя программа: простое произведение точки на матрицу.

Ref. : https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection#homogen-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
    )
  }

}

Затем используйте похожие треугольники

Ref. 1/2: Деталь. «О важности преобразования точек в пространство камеры» из https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/matMathematics-computing-2d-координаты-3d-точек

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

NB: на этом этапе записи представляют собой точки, выраженные в соответствии с камерой (т. Е .: они являются результатом предварительно определенного произведения с предварительно определенной матрицей).

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

  }

}

Наконец, нанесение проецируемых точек на холст.

Ref. : Часть. «От экранного пространства к растровому» из https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/matMathematics-computing-2d-координаты3D-точек

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

}

Вопрос

Что не так с моей программой? Я понял геометрические концепции, объясняемые в обоих руководствах, которые я внимательно прочитал. Я почти уверен, что мой продукт работает. Я думаю, что либо растеризация, либо записи (матрица) могут быть неправильными ...


person JarsOfJam-Scheduler    schedule 03.03.2018    source источник
comment
Просто чтобы убедиться, что я правильно понимаю: ваша проблема в том, что результирующие (прогнозируемые) координаты неверны. Точки (drawn_points), которые вы передаете на холст, уже преобразованы - верно?   -  person TobiSH    schedule 03.03.2018
comment
Дубликат: Моя неработающая реализация перспективной проекции   -  person Andrey Tyukin    schedule 04.03.2018
comment
@AndreyTyukin Это не дублирование: действительно, я повторно использовал (почти!) Тот же формат для обоих моих вопросов. Однако проблема совершенно в другом.   -  person JarsOfJam-Scheduler    schedule 04.03.2018
comment
@TobiSH Ваша проблема в том, что полученные (прогнозируемые) координаты неверны, вот и все, вы правы. Точки (drawn_points), которые вы передаете на холст, уже преобразованы - верно? : да. Сначала они преобразуются, чтобы быть выраженными в соответствии с системой координат камеры (изначально они выражаются в соответствии с мировой системой координат). Это цель матричного продукта. Во-вторых, Projector::drawPointOnCanvas использует похожие треугольники для проецирования точек, выраженных в соответствии с системой координат камеры, на холсте. (Холст просто норм. И растр)   -  person JarsOfJam-Scheduler    schedule 04.03.2018


Ответы (2)


Обратите внимание, что я использую простейшую матрицу перспективной проекции: я не использую концепцию fov, ближней и дальней плоскостей отсечения.

Я считаю, что ваша матрица проекции слишком проста. Отбрасывая ближнюю и дальнюю плоскости отсечения, вы полностью отбрасываете перспективную проекцию.

Вам не нужно выполнять шаг z-отсечения, но вам нужно определить усеченную пирамиду вида, чтобы перспектива работала. Я считаю, что ваша матрица проекции определяет кубическую "усеченную пирамиду", следовательно, никакой перспективы.

См. http://www.songho.ca/opengl/gl_projectionmatrix.html для обсуждение того, как работает матрица проекции.

person Rich    schedule 06.03.2018
comment
Думаю, ты ошибаешься. Действительно, в этом курсе также используется простая матрица проекции: github. com / ssloy / tinyrenderer / wiki / - person JarsOfJam-Scheduler; 06.03.2018
comment
Вам не кажется, что моя проблема связана с тем, что я не нормализовал значение координаты z в продукте projectionMatrix * point перед использованием аналогичной операции треугольников, как объяснено в Части переназначение координаты Z в scratchapixel .com / уроки / 3D-базовый-рендеринг /? Если да: я не знаю, зачем требуется это переназначение? - person JarsOfJam-Scheduler; 06.03.2018
comment
Хорошо, возможно я ошибаюсь. Я бы посоветовал попробовать оба подхода и посмотреть, какой из них работает. У меня сейчас нет времени пробовать это для вас. Я пока оставлю свой ответ в ожидании ваших выводов, но удалю его, если он окажется неправильным. Прости! - person Rich; 07.03.2018

Цитирование Scratchapixel страница:

... Если мы подставим эти числа в приведенное выше уравнение, мы получим:

введите описание изображения здесь

Где y' - координата y P'. Таким образом:

введите описание изображения здесь

Это, вероятно, одно из самых простых и фундаментальных соотношений в компьютерной графике, известное как z или перспективное деление. Тот же принцип применяется к координате x. ...

И в вашем коде:

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

Индекс (3) - это 4-й компонент point, то есть его W-координата, а не Z-координата. Может ты имел ввиду coordinate / point(2)?

person meowgoesthedog    schedule 11.03.2018
comment
Нет, 4-й компонент point равен его 3-му, поскольку две последние строки моей матрицы проекции: Seq(0, 0, -1, 0), Seq(0, 0, -1, 0). Однако я думаю, что нашел решение: ScratchaPixel на другой веб-странице своего сайта указывает, что матрица перспективной проекции заменяет все эти операции проекции (например, аналогичный треугольник). Возможно, мне действительно следует использовать матрицу, которая просто перемещает камеру (а не настоящую проекционную матрицу). Я попробую и обновлю этот вопрос. - person JarsOfJam-Scheduler; 11.03.2018
comment
Лично я в любом случае не предпочитаю матричный подход, потому что 1) слишком много противоречивых соглашений и 2) затрудняет отсечение в ближней плоскости (потребуется отбраковка в мировом пространстве, а не в пространстве камеры). - person meowgoesthedog; 11.03.2018
comment
Что ж, на первом этапе я не буду использовать матрицу перспективной проекции. Я действительно предпочитаю продолжать явно использовать аналогичные треугольники и т. Д. Я буду использовать точно данные ScratchaPixel (т.е. ту же матрицу перспективной проекции и те же координаты точек для рисования в 2D из 3D). Я обновлю этот вопрос на StackOverflow, когда у меня будет что-то новенькое :) - person JarsOfJam-Scheduler; 11.03.2018