Как преобразовать 3D-точку в 2D-проекцию перспективы?
В настоящее время я работаю с использованием кривых и поверхностей Безье, чтобы нарисовать знаменитый чайник Юты. Используя патчи Безье из 16 контрольных точек, я смог нарисовать чайник и отобразить его с помощью функции "мир в камеру", которая дает возможность вращать полученный чайник, и в настоящее время использую ортогональную проекцию.
Результатом является то, что у меня есть "плоский" чайник, который ожидается, поскольку цель ортогональной проекции-сохранить параллельность линии.Однако я хотел бы использовать проекцию перспективы, чтобы придать чайнику глубину. Мой вопрос заключается в том, как взять вершину 3D xyz, возвращенную из функции "мир в камеру", и преобразовать ее в 2D-координату. Я хочу использовать плоскость проекции при z=0 и позволить пользователю определять фокусное расстояние и размер изображения с помощью клавиш со стрелками на клавиатуре.
Я программирую это на java и настроил все обработчики входных событий, а также написал класс matrix, который обрабатывает базовое умножение матриц. Я уже некоторое время читаю "Википедию" и другие ресурсы, но не могу понять, как человек выполняет эту трансформацию.
10 ответов:
Я вижу, что этот вопрос немного устарел, но я решил дать ответ в любом случае для тех, кто находит этот вопрос путем поиска.
Стандартный способ представления 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. это называется перспективным делением.
Теперь ваши вершины находятся в пространстве обрезки, и вы хотите выполнить обрезку, чтобы не выводить пиксели за пределы видового экрана. Сазерленд-Ходжмен отсечение является наиболее распространенным используемым алгоритмом отсечения.- преобразуйте 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