Как преобразовать 3D точку в 2D перспективную проекцию? - PullRequest
48 голосов
/ 07 апреля 2009

В настоящее время я работаю с использованием кривых Безье и поверхностей для рисования знаменитого чайника из Юты. Используя патчи Безье из 16 контрольных точек, я смог нарисовать чайник и отобразить его с помощью функции «мир в камеру», которая дает возможность вращать полученный чайник, и в настоящее время я использую ортографическую проекцию.

В результате у меня есть «плоский» чайник, который, как ожидается, предназначен для ортогональной проекции - сохранить параллельные линии.

Однако я хотел бы использовать перспективную проекцию, чтобы определить глубину чайника. Мой вопрос: как взять 3D-вершину XYZ, возвращенную из функции «мир в камеру», и преобразовать ее в 2D-координату. Я хочу использовать плоскость проекции при z = 0 и позволить пользователю определять фокусное расстояние и размер изображения с помощью клавиш со стрелками на клавиатуре.

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

Ответы [ 10 ]

86 голосов
/ 15 мая 2009

Я вижу, что этот вопрос немного устарел, но я все равно решил дать ответ тем, кто найдет этот вопрос с помощью поиска.
В настоящее время стандартным способом представления 2D / 3D преобразований является использование однородных координат . [x, y, w] для 2D и [x, y, z, w] для 3D. Поскольку у вас есть три оси в 3D и перевод, эта информация идеально вписывается в матрицу преобразования 4x4. Я буду использовать обозначение матрицы столбцов в этом объяснении. Все матрицы имеют размер 4х4, если не указано иное.
Этапы от трехмерных точек до растеризованной точки, линии или многоугольника выглядят так:

  1. Преобразуйте ваши 3D-точки с помощью матрицы обратной камеры, а затем выполните любые необходимые преобразования. Если у вас есть поверхностные нормали, также преобразуйте их, но с w, установленным в ноль, так как вы не хотите переводить нормали. Матрица, с которой вы преобразуете нормали, должна быть изотропной ; скейлинг и сдвиг приводят к искажению нормалей.
  2. Преобразование точки с помощью матрицы пространства клипа. Эта матрица масштабирует x и y с полем зрения и соотношением сторон, масштабирует z по ближним и дальним плоскостям отсечения и вставляет «старый» z в w. После преобразования вы должны разделить x, y и z на w. Это называется перспективный разрыв .
  3. Теперь ваши вершины находятся в пространстве клипа, и вы хотите выполнить отсечение, чтобы не отображать пиксели за пределами области просмотра. Отсечение по Сазерленду-Ходжману - самый распространенный используемый алгоритм отсечения.
  4. Преобразуйте x и y относительно w, полуширины и полувысоты. Ваши координаты x и y теперь находятся в координатах области просмотра. w отбрасывается, но 1 / w и z обычно сохраняются, потому что 1 / w требуется для правильной интерполяции по поверхности полигона, а z сохраняется в z-буфере и используется для проверки глубины.

Этот этап является фактической проекцией, поскольку z больше не используется в качестве компонента в позиции.

Алгоритмы:

Расчет поля зрения

Это вычисляет поле зрения. Независимо от того, принимает ли загар радианы или градусы, значение не должно совпадать. Обратите внимание, что результат достигает бесконечности, так как угол приближается к 180 градусам. Это особенность, так как невозможно иметь такой широкий фокус. Если вам нужна числовая стабильность, оставьте angle меньше или равным 179 градусам.

fov = 1.0 / tan(angle/2.0)

Также обратите внимание, что 1.0 / tan (45) = 1. Кто-то еще здесь предложил просто разделить на z. Результат здесь понятен. Вы получите 90 градусов FOV и соотношение сторон 1: 1. Использование подобных однородных координат также имеет несколько других преимуществ; например, мы можем выполнить отсечение для ближней и дальней плоскостей, не рассматривая ее как особый случай.

Расчет матрицы клипа

Это макет матрицы клипа. аспектное отношение - это ширина / высота. Таким образом, FOV для компонента x масштабируется на основе FOV для y. Далеко и близко - коэффициенты, которые являются расстояниями для ближнего и дальнего отсечения.

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][        1       ]
[        0        ][        0        ][(2*near*far)/(near-far)][        0       ]

Проекция экрана

После отсечения это последнее преобразование для получения координат экрана.

new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;

Тривиальный пример реализации в C ++

#include <vector>
#include <cmath>
#include <stdexcept>
#include <algorithm>

struct Vector
{
    Vector() : x(0),y(0),z(0),w(1){}
    Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){}

    /* Assume proper operator overloads here, with vectors and scalars */
    float Length() const
    {
        return std::sqrt(x*x + y*y + z*z);
    }

    Vector Unit() const
    {
        const float epsilon = 1e-6;
        float mag = Length();
        if(mag < epsilon){
            std::out_of_range e("");
            throw e;
        }
        return *this / mag;
    }
};

inline float Dot(const Vector& v1, const Vector& v2)
{
    return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}

class Matrix
{
    public:
    Matrix() : data(16)
    {
        Identity();
    }
    void Identity()
    {
        std::fill(data.begin(), data.end(), float(0));
        data[0] = data[5] = data[10] = data[15] = 1.0f;
    }
    float& operator[](size_t index)
    {
        if(index >= 16){
            std::out_of_range e("");
            throw e;
        }
        return data[index];
    }
    Matrix operator*(const Matrix& m) const
    {
        Matrix dst;
        int col;
        for(int y=0; y<4; ++y){
            col = y*4;
            for(int x=0; x<4; ++x){
                for(int i=0; i<4; ++i){
                    dst[x+col] += m[i+col]*data[x+i*4];
                }
            }
        }
        return dst;
    }
    Matrix& operator*=(const Matrix& m)
    {
        *this = (*this) * m;
        return *this;
    }

    /* The interesting stuff */
    void SetupClipMatrix(float fov, float aspectRatio, float near, float far)
    {
        Identity();
        float f = 1.0f / std::tan(fov * 0.5f);
        data[0] = f*aspectRatio;
        data[5] = f;
        data[10] = (far+near) / (far-near);
        data[11] = 1.0f; /* this 'plugs' the old z into w */
        data[14] = (2.0f*near*far) / (near-far);
        data[15] = 0.0f;
    }

    std::vector<float> data;
};

inline Vector operator*(const Vector& v, const Matrix& m)
{
    Vector dst;
    dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12];
    dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13];
    dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
    dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
    return dst;
}

typedef std::vector<Vector> VecArr;
VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr& vertex)
{
    float halfWidth = (float)width * 0.5f;
    float halfHeight = (float)height * 0.5f;
    float aspect = (float)width / (float)height;
    Vector v;
    Matrix clipMatrix;
    VecArr dst;
    clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far);
    /*  Here, after the perspective divide, you perform Sutherland-Hodgeman clipping 
        by checking if the x, y and z components are inside the range of [-w, w].
        One checks each vector component seperately against each plane. Per-vertex
        data like colours, normals and texture coordinates need to be linearly
        interpolated for clipped edges to reflect the change. If the edge (v0,v1)
        is tested against the positive x plane, and v1 is outside, the interpolant
        becomes: (v1.x - w) / (v1.x - v0.x)
        I skip this stage all together to be brief.
    */
    for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){
        v = (*i) * clipMatrix;
        v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/
        dst.push_back(v);
    }

    /* TODO: Clipping here */

    for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){
        i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
        i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
    }
    return dst;
}

Если вы все еще задумываетесь над этим, спецификация OpenGL - действительно хороший справочник для математики. На форумах DevMaster на http://www.devmaster.net/ есть много хороших статей, связанных также с программными растеризаторами.

12 голосов
/ 07 апреля 2009

Я думаю этот , вероятно, ответит на ваш вопрос. Вот что я там написал:

Вот очень общий ответ. Скажите камере на (Xc, Yc, Zc) и точка, которую вы хотите проецировать, это P = (X, Y, Z). Расстояние от камеры до 2D-плоскости, на которую вы проецируете, равно F (поэтому уравнение плоскости равно Z-Zc = F). 2D координаты P, спроецированные на плоскость, (X ', Y').

Тогда очень просто:

X '= ((X - Xc) * (F / Z)) + Xc

Y '= ((Y - Yc) * (F / Z)) + Yc

Если ваша камера является исходной, то это упрощается до:

X '= X * (F / Z) * ​​1016 *

Y '= Y * (F / Z) * ​​1018 *

6 голосов
/ 19 апреля 2013

Вы можете проецировать трехмерную точку в 2D, используя: Математика Commons: Математическая библиотека Apache Commons только с двумя классами.

Пример для Java Swing.

import org.apache.commons.math3.geometry.euclidean.threed.Plane;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;


Plane planeX = new Plane(new Vector3D(1, 0, 0));
Plane planeY = new Plane(new Vector3D(0, 1, 0)); // Must be orthogonal plane of planeX

void drawPoint(Graphics2D g2, Vector3D v) {
    g2.drawLine(0, 0,
            (int) (world.unit * planeX.getOffset(v)),
            (int) (world.unit * planeY.getOffset(v)));
}

protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    drawPoint(g2, new Vector3D(2, 1, 0));
    drawPoint(g2, new Vector3D(0, 2, 0));
    drawPoint(g2, new Vector3D(0, 0, 2));
    drawPoint(g2, new Vector3D(1, 1, 1));
}

Теперь вам нужно всего лишь обновить planeX и planeY, чтобы изменить перспективную проекцию, чтобы получить такие вещи:

enter image description hereenter image description here

5 голосов
/ 07 апреля 2009

Чтобы получить координаты с поправкой на перспективу, просто поделите на z координату:

xc = x / z
yc = y / z

Вышеуказанное работает при условии, что камера находится на (0, 0, 0), а вы проецируетесь на плоскость на z = 1 - в противном случае вам необходимо перевести координаты относительно камеры.

Существуют некоторые сложности для кривых, поскольку проецирование точек кривой Безье в 3D не дает в общем тех же точек, что и при рисовании 2D кривой Безье через проецируемые точки.

2 голосов
/ 17 августа 2016

enter image description here

Глядя на экран сверху, вы получаете оси X и Z.
Глядя на экран сбоку, вы получаете оси Y и Z.

Рассчитайте фокусные расстояния для вида сверху и сбоку, используя тригонометрию, которая представляет собой расстояние между глазом и серединой экрана, которое определяется полем обзора экрана. Это делает форму двух прямоугольных треугольников вплотную.

hw = screen_width / 2

чч = screen_height / 2

fl_top = hw / tan (θ / 2)

fl_side = чч / загар (θ / 2)


Затем возьмите среднее фокусное расстояние.

fl_average = (fl_top + fl_side) / 2


Теперь вычислите новые x и new y с помощью основной арифметики, поскольку больший прямоугольный треугольник, созданный из точки 3d и точки глаза, совпадает с меньшим треугольником, образованным точкой 2d и точкой глаза.

x '= (x * fl_top) / (z + fl_top)

y '= (y * fl_top) / (z + fl_top)


Или вы можете просто установить

x '= x / (z + 1)

и

y '= y / (z + 1)

1 голос
/ 08 мая 2016

Все ответы касаются поставленного вопроса в заголовке . Тем не менее, я хотел бы добавить оговорку, которая неявно в тексте . Патчи Безье используются для представления поверхности, но вы не можете просто преобразовать точки патча и тесселяции патча в полигоны, потому что это приведет к искаженной геометрии. Однако вы можете сначала выполнить тесселяцию патча в многоугольники с использованием преобразованного допуска экрана, а затем трансформировать многоугольники, либо вы можете преобразовать патчи Безье в рациональные патчи Безье, а затем тесселировать те, которые используют допуск на экранное пространство. Первое проще, а второе лучше для производственной системы.

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

1 голос
/ 07 апреля 2009

Я не уверен, на каком уровне вы задаете этот вопрос. Похоже, вы нашли формулы в Интернете и просто пытаетесь понять, что они делают. На том чтении вашего вопроса я предлагаю:

  • Представьте себе луч от зрителя (в точке V) прямо к центру плоскости проекции (назовите его C).
  • Представьте себе второй луч от зрителя до точки на изображении (P), которая также пересекает плоскость проекции в некоторой точке (Q)
  • Зритель и две точки пересечения на плоскости обзора образуют треугольник (VCQ); стороны - это два луча и линия между точками на плоскости.
  • Формулы используют этот треугольник, чтобы найти координаты Q, куда будет направлен проецируемый пиксель
0 голосов
/ 30 апреля 2017

Спасибо @Mads Elvenheim за правильный пример кода. Я исправил незначительные синтаксические ошибки в коде (всего несколько const проблем и очевидных пропущенных операторов). Кроме того, около и далеко имеют совершенно разные значения по сравнению с

Для вашего удовольствия, вот скомпилированная (MSVC2013) версия. Повеселись. Помните, что я установил NEAR_Z и FAR_Z постоянными. Вы, вероятно, не хотите этого.

#include <vector>
#include <cmath>
#include <stdexcept>
#include <algorithm>

#define M_PI 3.14159

#define NEAR_Z 0.5
#define FAR_Z 2.5

struct Vector
{
    float x;
    float y;
    float z;
    float w;

    Vector() : x( 0 ), y( 0 ), z( 0 ), w( 1 ) {}
    Vector( float a, float b, float c ) : x( a ), y( b ), z( c ), w( 1 ) {}

    /* Assume proper operator overloads here, with vectors and scalars */
    float Length() const
    {
        return std::sqrt( x*x + y*y + z*z );
    }
    Vector& operator*=(float fac) noexcept
    {
        x *= fac;
        y *= fac;
        z *= fac;
        return *this;
    }
    Vector  operator*(float fac) const noexcept
    {
        return Vector(*this)*=fac;
    }
    Vector& operator/=(float div) noexcept
    {
        return operator*=(1/div);   // avoid divisions: they are much
                                    // more costly than multiplications
    }

    Vector Unit() const
    {
        const float epsilon = 1e-6;
        float mag = Length();
        if (mag < epsilon) {
            std::out_of_range e( "" );
            throw e;
        }
        return Vector(*this)/=mag;
    }
};

inline float Dot( const Vector& v1, const Vector& v2 )
{
    return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}

class Matrix
{
public:
    Matrix() : data( 16 )
    {
        Identity();
    }
    void Identity()
    {
        std::fill( data.begin(), data.end(), float( 0 ) );
        data[0] = data[5] = data[10] = data[15] = 1.0f;
    }
    float& operator[]( size_t index )
    {
        if (index >= 16) {
            std::out_of_range e( "" );
            throw e;
        }
        return data[index];
    }
    const float& operator[]( size_t index ) const
    {
        if (index >= 16) {
            std::out_of_range e( "" );
            throw e;
        }
        return data[index];
    }
    Matrix operator*( const Matrix& m ) const
    {
        Matrix dst;
        int col;
        for (int y = 0; y<4; ++y) {
            col = y * 4;
            for (int x = 0; x<4; ++x) {
                for (int i = 0; i<4; ++i) {
                    dst[x + col] += m[i + col] * data[x + i * 4];
                }
            }
        }
        return dst;
    }
    Matrix& operator*=( const Matrix& m )
    {
        *this = (*this) * m;
        return *this;
    }

    /* The interesting stuff */
    void SetupClipMatrix( float fov, float aspectRatio )
    {
        Identity();
        float f = 1.0f / std::tan( fov * 0.5f );
        data[0] = f*aspectRatio;
        data[5] = f;
        data[10] = (FAR_Z + NEAR_Z) / (FAR_Z- NEAR_Z);
        data[11] = 1.0f; /* this 'plugs' the old z into w */
        data[14] = (2.0f*NEAR_Z*FAR_Z) / (NEAR_Z - FAR_Z);
        data[15] = 0.0f;
    }

    std::vector<float> data;
};


inline Vector operator*( const Vector& v, Matrix& m )
{
    Vector dst;
    dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8] + v.w*m[12];
    dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9] + v.w*m[13];
    dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
    dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
    return dst;
}

typedef std::vector<Vector> VecArr;
VecArr ProjectAndClip( int width, int height, const VecArr& vertex )
{
    float halfWidth = (float)width * 0.5f;
    float halfHeight = (float)height * 0.5f;
    float aspect = (float)width / (float)height;
    Vector v;
    Matrix clipMatrix;
    VecArr dst;
    clipMatrix.SetupClipMatrix( 60.0f * (M_PI / 180.0f), aspect);
    /*  Here, after the perspective divide, you perform Sutherland-Hodgeman clipping
    by checking if the x, y and z components are inside the range of [-w, w].
    One checks each vector component seperately against each plane. Per-vertex
    data like colours, normals and texture coordinates need to be linearly
    interpolated for clipped edges to reflect the change. If the edge (v0,v1)
    is tested against the positive x plane, and v1 is outside, the interpolant
    becomes: (v1.x - w) / (v1.x - v0.x)
    I skip this stage all together to be brief.
    */
    for (VecArr::const_iterator i = vertex.begin(); i != vertex.end(); ++i) {
        v = (*i) * clipMatrix;
        v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/
        dst.push_back( v );
    }

    /* TODO: Clipping here */

    for (VecArr::iterator i = dst.begin(); i != dst.end(); ++i) {
        i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
        i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
    }
    return dst;
}
#pragma once
0 голосов
/ 29 января 2014

Возможно, вы захотите отладить вашу систему с помощью сфер, чтобы определить, есть ли у вас хорошее поле зрения. Если оно слишком широкое, сферы с краями экрана деформируются в более овальные формы, направленные к центру рамки. Решение этой проблемы состоит в том, чтобы увеличить масштаб кадра, умножив координаты x и y для трехмерной точки на скаляр, а затем уменьшив ваш объект или мир на аналогичный коэффициент. Тогда вы получите красивую ровную сферу по всему кадру.

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

Тем не менее, важность калибровки коэффициента масштабирования кадра при визуализации сфер невозможно переоценить. Если вы не знаете, где находится «обитаемая зона» вашей вселенной, вы в конечном итоге будете ходить по солнцу и отказываться от проекта. Вы хотите иметь возможность визуализировать сферу в любом месте вашей рамки зрения, если она выглядит круглой. В моем проекте единичная сфера огромна по сравнению с регионом, который я описываю.

Также обязательная запись в википедии: Сферическая система координат

0 голосов
/ 22 июля 2010

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

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][(2*near*far)/(near-far)]
[        0        ][        0        ][        1              ][        0       ]

какое-то дополнение к твоим вещам:

Эта матрица клипов работает, только если вы проецируете на статическую 2D-плоскость, если хотите добавить движение и вращение камеры:

viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;

это позволяет вращать 2D-плоскость и перемещать ее вокруг ..-

...