Как преобразовать 3D-точку в 2D-проекцию перспективы?


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

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

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

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

10 44

10 ответов:

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

  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 градусам. Это сингулярность, поскольку невозможно иметь столь широкую фокусную точку. Если вы хотите численную стабильность, держите угол меньше или равным 179 градусам.

fov = 1.0 / tan(angle/2.0)

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

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

Это макет матрицы клипов. aspectRatio - это ширина/высота. Итак, FOV для x компонент масштабируется на основе FOV для y. Far и near-коэффициенты, которые являются расстояниями для ближней и дальней плоскостей отсечения.

[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/ есть много хороших статей, связанных с программными растризаторами, а также.

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

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

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

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

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

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

X' = X * (F/Z)

Y' = Y * (F/Z)

Вы можете спроецировать 3D-точку в 2D с помощью: Commons Math: библиотека Apache Commons Mathematics всего с двумя классами.

Пример для 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, чтобы изменить перспективу-проекцию, чтобы получить вещи, подобные этому:

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

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

xc = x / z
yc = y / z

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

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

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

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

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

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

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

Hw = screen_width / 2

Hh = screen_height / 2

Fl_top = hw / tan (θ/2)

Fl_side = hh / tan (θ/2)


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

Fl_average = (fl_top + fl_side) / 2


Теперь посчитаем новый и новый X Y с элементарной арифметики, так как большой прямоугольный треугольник, сделанный из 3D точки и эта точка совпадает с меньшего треугольника, сделанная в 2Д и точка глаза.

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

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


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

X' = x / (z + 1)

И

Y' = y / (z + 1)

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

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

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

[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-плоскость и перемещать ее..-

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

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

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

Кроме того, обязательная запись в Википедии: Сферическая Система Координат

Спасибо @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