Подтвердить что ты не робот

Как преобразовать трехмерную точку в двумерную перспективную проекцию?

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

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

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

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

4b9b3361

Ответ 1

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

  • Преобразуйте свои 3D-точки с помощью матрицы обратных камер, затем следуйте любым преобразованиям, в которых они нуждаются. Если у вас есть нормали поверхности, преобразуйте их также, но с w установите в ноль, так как вы не хотите переводить нормали. Матрица, которую вы преобразовываете нормали, должна быть изотропной; масштабирование и стрижка делают нормали неправильными.
  • Преобразование точки с помощью матрицы пространства клипов. Эта матрица масштабирует x и y с полем зрения и соотношением сторон, масштабов z на плоскости ближнего и дальнего отсечения и вставляет "старую" z в w. После преобразования вы должны разделить x, y и z на w. Это называется перспективой.
  • Теперь ваши вершины находятся в пространстве клипов, и вы хотите выполнить обрезку, чтобы вы не отображали пиксели вне границ области просмотра. Обрезка Sutherland-Hodgeman - наиболее распространенный алгоритм отсечения при использовании.
  • Преобразование 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. Далекие и близкие коэффициенты, которые являются расстояниями для ближней и дальней плоскости отсечения.

[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;

Тривиальная реализация примера в С++

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

Ответ 2

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

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

Ответ 3

Вы можете проецировать трехмерную точку в 2D, используя: Commons Math: Библиотека математики 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

Ответ 4

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

xc = x / z
yc = y / z

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

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

Предполагая, что FOV составляет 90 градусов,

то следующее упрощается:

x '= (x/tan (fov/2))/(z + (1/tan (fov/2)))

y '= (y/tan (fov/2))/(z + (1/tan (fov/2)))

To:

x '= x/(z + 1)

y '= y/(z + 1)

-

Но начало координат не в (0,0,0), поэтому следующее упрощается:

x '= (x * (screen_width/2)/tan (fov/2))/(z + ((screen_width/2)/tan (fov/2)))

y '= (y * (screen_height/2)/tan (fov/2))/(z + ((screen_height/2)/tan (fov/2)))

-

To:

x '= (x * (screen_width/2))/(z + (screen_width/2))

y '= (y * (screen_height/2))/(z + (screen_height/2))

-

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

-

average_len = (screen_width + screen_height)/2

x '= (x * (average_len/2))/(z + (average_len/2))

y '= (y * (average_len/2))/(z + (average_len/2))

Ответ 8

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

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

Ответ 9

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

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

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

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

Ответ 10

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

Для вашего удовольствия, вот компилируемая версия (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