java - tridimensionales - ¿Cómo convertir un punto 3D en una proyección en perspectiva 2D?
proyecciones tridimensionales (10)
Creo que this probablemente responda tu pregunta. Esto es lo que escribí allí:
Aquí hay una respuesta muy general. Supongamos que la cámara está en (Xc, Yc, Zc) y el punto que desea proyectar es P = (X, Y, Z). La distancia desde la cámara al plano 2D en el que está proyectando es F (por lo que la ecuación del plano es Z-Zc = F). Las coordenadas 2D de P proyectadas en el plano son (X '', Y'').
Entonces, muy simple:
X ''= ((X - Xc) * (F / Z)) + Xc
Y ''= ((Y - Yc) * (F / Z)) + Yc
Si su cámara es el origen, entonces esto se simplifica a:
X ''= X * (F / Z)
Y ''= Y * (F / Z)
Actualmente estoy trabajando con el uso de curvas y superficies Bezier para dibujar la famosa tetera de Utah. Usando parches Bezier de 16 puntos de control, pude dibujar la tetera y mostrarla usando la función ''mundo a cámara'' que da la capacidad de rotar la tetera resultante, y actualmente estoy usando una proyección ortográfica.
El resultado es que tengo una tetera ''plana'', que se espera ya que el propósito de una proyección ortográfica es preservar líneas paralelas.
Sin embargo, me gustaría usar una proyección en perspectiva para darle profundidad a la tetera. Mi pregunta es, ¿cómo se toma el vértice xyz 3D devuelto por la función ''mundo a cámara'', y se convierte en una coordenada 2D. Deseo utilizar el plano de proyección en z = 0 y permitir que el usuario determine la distancia focal y el tamaño de la imagen con las teclas de flecha del teclado.
Estoy programando esto en Java y tengo todo el controlador de eventos de entrada configurado, y también he escrito una clase de matriz que maneja la multiplicación de matrices básicas. He estado leyendo wikipedia y otros recursos durante un tiempo, pero no puedo entender cómo se realiza esta transformación.
Es posible que desee depurar su sistema con esferas para determinar si tiene o no un buen campo de visión. Si lo tiene demasiado ancho, las esferas se deforman en los bordes de la pantalla en formas más ovales apuntando hacia el centro del marco. La solución a este problema es hacer un acercamiento al marco, multiplicando las coordenadas x e y por el punto tridimensional por un escalar y luego reduciendo su objeto o mundo por un factor similar. Luego obtienes la bonita esfera redonda en todo el cuadro.
Casi me avergüenzo de que me haya llevado todo el día resolver esto y estuve casi convencido de que había un misterioso fenómeno geométrico misterioso sucediendo aquí que exigía un enfoque diferente.
Sin embargo, no se puede exagerar la importancia de calibrar el coeficiente zoom-frame-of-view mediante la representación de esferas. Si no sabe dónde está la "zona habitable" de su universo, terminará caminando sobre el sol y descartando el proyecto. Desea poder representar una esfera en cualquier lugar en su marco de visión y hacer que parezca redonda. En mi proyecto, la unidad de esfera es masiva en comparación con la región que estoy describiendo.
Además, la entrada obligatoria de wikipedia: Sistema de coordenadas esféricas
Gracias a @Mads Elvenheim por un código de ejemplo adecuado. He corregido los errores de sintaxis menores en el código (solo algunos problemas de const y obviamente faltan operadores). Además, cerca y lejos tienen significados muy diferentes en vs.
Para su placer, aquí está la versión compilable (MSVC2013). Que te diviertas. Tenga en cuenta que he hecho que NEAR_Z y FAR_Z sean constantes. Probablemente no lo quieras así.
#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
Mi respuesta anterior fue incorrecta y basura.
Aquí hay un rehacer:
Mirando la pantalla desde la parte superior, obtienes los ejes xyz.
Mirando la pantalla desde un lado, obtienes los ejes yy z.
Calcule las distancias focales de las vistas superior y lateral, usando la trigonometría, que es la distancia entre el ojo y el medio de la pantalla, que está determinada por el campo de visión de la pantalla. Esto hace que la forma de dos triángulos rectángulos espalda con espalda.
hw = screen_width / 2
hh = screen_height / 2
fl_top = hw / tan (θ / 2)
fl_side = hh / tan (θ / 2)
Luego tome la distancia focal promedio.
fl_average = (fl_top + fl_side) / 2
Ahora calcule la nueva x y la nueva y con la aritmética básica, ya que el triángulo rectángulo más grande hecho desde el punto 3d y el punto del ojo es congruente con el triángulo más pequeño formado por el punto 2d y el punto del ojo.
x ''= (x * fl_top) / (z + fl_top)
y ''= (y * fl_top) / (z + fl_top)
O simplemente puede establecer
x ''= x / (z + 1)
y
y ''= y / (z + 1)
No estoy seguro en qué nivel estás haciendo esta pregunta. Parece que ha encontrado las fórmulas en línea y solo está tratando de entender qué hace. En esa lectura de su pregunta ofrezco:
- Imagine un rayo del espectador (en el punto V) directamente hacia el centro del plano de proyección (llámelo C).
- Imagine un segundo rayo desde el visor hasta un punto en la imagen (P) que también intersecta el plano de proyección en algún punto (Q)
- El espectador y los dos puntos de intersección en el plano de vista forman un triángulo (VCQ); los lados son los dos rayos y la línea entre los puntos en el plano.
- Las fórmulas están usando este triángulo para encontrar las coordenadas de Q, que es donde irá el píxel proyectado
Para obtener las coordenadas corregidas en perspectiva, solo divida por la coordenada z
:
xc = x / z
yc = y / z
Lo anterior funciona suponiendo que la cámara está en (0, 0, 0)
y se está proyectando en el plano en z = 1
; de lo contrario, debe traducir los co-ords relativos a la cámara.
Hay algunas complicaciones para las curvas, en la medida en que proyectar los puntos de una curva 3D Bezier no le proporcionará en general los mismos puntos que dibujar una curva Bezier 2D a través de los puntos proyectados.
Puede proyectar un punto 3D en 2D usando: Commons Math: Apache Commons Mathematics Library con solo dos clases.
Ejemplo para 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));
}
Ahora solo necesita actualizar el planeX
y el planeY
para cambiar la perspectiva-proyección, para obtener cosas como esta:
Sé que es un tema antiguo, pero su ilustración no es correcta, el código fuente configura la matriz de clips correcta.
[fov * aspectRatio][ 0 ][ 0 ][ 0 ]
[ 0 ][ fov ][ 0 ][ 0 ]
[ 0 ][ 0 ][(far+near)/(far-near) ][(2*near*far)/(near-far)]
[ 0 ][ 0 ][ 1 ][ 0 ]
alguna adición a tus cosas:
Esta matriz de clip funciona solo si está proyectando en plano 2D estático si desea agregar movimiento y rotación de la cámara:
viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;
esto le permite rotar el plano 2D y moverlo ..-
Todas las respuestas abordan la pregunta planteada en el título . Sin embargo, me gustaría agregar una advertencia implícita en el texto . Los parches de Bézier se usan para representar la superficie, pero no se puede simplemente transformar los puntos del parche y teselar el parche en polígonos, ya que esto generará una geometría distorsionada. Sin embargo, puede teselar el parche primero en polígonos utilizando una tolerancia de pantalla transformada y luego transformar los polígonos, o puede convertir los parches de Bézier en parches Bézier racionales, y luego teselar los que usan una tolerancia de espacio de pantalla. El primero es más fácil, pero el último es mejor para un sistema de producción.
Sospecho que quieres el camino más fácil. Para ello, escalaría la tolerancia de la pantalla según la norma de Jacobian de la transformación de perspectiva inversa y la usaría para determinar la cantidad de teselación que necesita en el espacio modelo (podría ser más fácil calcular el Jacobian directo, invertirlo, luego toma la norma). Tenga en cuenta que esta norma depende de la posición, y es posible que desee evaluar esto en varias ubicaciones, según la perspectiva. También recuerde que dado que la transformación proyectiva es racional, debe aplicar la regla del cociente para calcular las derivadas.
Veo que esta pregunta es un poco vieja, pero decidí dar una respuesta de todos modos para aquellos que encuentran esta pregunta buscando.
La forma estándar de representar transformaciones 2D / 3D hoy en día es mediante el uso de coordenadas homogéneas . [x, y, w] para 2D, y [x, y, z, w] para 3D. Como tiene tres ejes en 3D y también traducción, esa información se adapta perfectamente a una matriz de transformación 4x4. Usaré la notación de matriz de columna principal en esta explicación. Todas las matrices son 4x4 a menos que se indique lo contrario.
Las etapas de los puntos 3D y de un punto, línea o polígono rasterizado se ven así:
- Transforme sus puntos 3D con la matriz de la cámara inversa, seguidos de las transformaciones que necesiten. Si tienes normales de superficie, transfórmalas también pero con w puesto a cero, ya que no quieres traducir normales. La matriz con la que transformas las normales debe ser isotrópica ; escalar y esquilar hace que las normales tengan malformaciones.
- Transforma el punto con una matriz de espacio de clip. Esta matriz escala xey con el campo de visión y la relación de aspecto, escalas z por los planos de recorte cercano y lejano, y conecta la ''vieja'' z en w. Después de la transformación, debe dividir x, y y z por w. Esto se llama división de perspectiva .
- Ahora sus vértices están en el espacio del clip, y desea realizar un recorte para que no muestre píxeles fuera de los límites de la ventana gráfica. El recorte de Sutherland-Hodgeman es el algoritmo de recorte más extendido en uso.
- Transforma xey con respecto a w y el ancho medio y la media altura. Sus coordenadas xey están ahora en coordenadas de vista. w se descarta, pero 1 / wyz generalmente se guardan porque se requiere 1 / w para hacer una interpolación correcta de perspectiva a través de la superficie del polígono, y z se almacena en el z-buffer y se usa para la prueba de profundidad.
Esta etapa es la proyección real, porque z ya no se usa como un componente en la posición.
Los algoritmos:
Cálculo del campo de visión
Esto calcula el campo de visión. Si el bronceado necesita radianes o grados es irrelevante, pero el ángulo debe coincidir. Observe que el resultado llega al infinito cuando el ángulo se acerca a 180 grados. Esta es una singularidad, ya que es imposible tener un punto focal tan amplio. Si desea estabilidad numérica, mantenga un ángulo menor o igual a 179 grados.
fov = 1.0 / tan(angle/2.0)
También note que 1.0 / tan (45) = 1. Alguien más aquí sugirió simplemente dividir por z. El resultado aquí es claro. Obtendrá un FOV de 90 grados y una relación de aspecto de 1: 1. El uso de coordenadas homogéneas como esta tiene también otras ventajas; podemos, por ejemplo, realizar clipping contra los planos cercano y lejano sin tratarlo como un caso especial.
Cálculo de la matriz de clip
Este es el diseño de la matriz de clip. aspectRatio es Ancho / Altura. Por lo tanto, el FOV para el componente x se escala en función del campo de visión para y. Lejos y cercanos son los coeficientes que son las distancias para los planos de recorte cercanos y lejanos.
[fov * aspectRatio][ 0 ][ 0 ][ 0 ]
[ 0 ][ fov ][ 0 ][ 0 ]
[ 0 ][ 0 ][(far+near)/(far-near) ][ 1 ]
[ 0 ][ 0 ][(2*near*far)/(near-far)][ 0 ]
Proyección de pantalla
Después del recorte, esta es la transformación final para obtener nuestras coordenadas de pantalla.
new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;
Implementación de ejemplo trivial en 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;
}
Si todavía reflexiona sobre esto, la especificación OpenGL es una referencia muy buena para las matemáticas involucradas. Los foros de DevMaster en http://www.devmaster.net/ tienen muchos buenos artículos relacionados con rasterizadores de software también.