opengl
Vista y proyección OGL
Buscar..
Introducción
Sobre matriz de modelo, matriz de vista, proyección ortográfica y perspectiva.
Implementar una cámara en OGL 4.0 GLSL 400.
Si queremos ver una escena como si la hubiéramos fotografiado con una cámara, primero debemos definir algunas cosas:
- La posición desde la cual se ve la escena, la posición del ojo
pos
. - El punto que miramos en la escena (
target
). También es común definir la dirección en la que miramos. Técnicamente necesitamos una línea de visión . Una recta en el espacio se define matemáticamente por 2 puntos o por un punto y un vector. La primera parte de la definición es la posición del ojo y la segunda es eltarget
o el vector de línea de visiónlos
. - La dirección hacia
up
haciaup
. - El campo de visión '
fov_y
. Esto significa el ángulo entre las dos líneas rectas, comenzando en la posición del ojo y terminando en el punto más a la izquierda y el más a la derecha, que se puede ver simultáneamente. - El gran tamaño y la relación de aspecto de la ventana gráfica a la que proyectamos nuestra imagen
vp
. - El plano
near
y el planofar
. El plano cercano es la distancia desde la posición del ojo al plano desde donde los objetos se hacen visibles para nosotros. El plano lejano es la distancia desde la posición del ojo al plano en el que los objetos de la escena son visibles para nosotros. Una explicación de lo que se necesita el plano cercano y el plano lejano seguirá más adelante.
Una definición de estos datos en C ++ y en Python puede tener este aspecto:
C ++
using TVec3 = std::array<float,3>;
struct Camera
{
TVec3 pos {0.0, -8.0, 0.0};
TVec3 target {0.0, 0.0, 0.0};
TVec3 up {0.0, 0.0, 1.0};
float fov_y {90.0};
TSize vp {800, 600};
float near {0.5};
float far {100.0};
};
Pitón
class Camera:
def __init__(self):
self.pos = (0, -8, 0)
self.target = (0, 0, 0)
self.up = (0, 0, 1)
self.fov_y = 90
self.vp = (800, 600)
self.near = 0.5
self.far = 100.0
Para tomar en cuenta toda esta información al dibujar una escena, generalmente se usan una matriz de proyección y una matriz de vista. Para organizar las partes individuales de una escena en la escena, se utilizan matrices modelo. Sin embargo, estos se mencionan aquí solo por estar completos y no se tratarán aquí.
Matriz de proyección: la matriz de proyección describe el mapeo desde puntos 3D en el mundo tal como se ven desde una cámara estenopeica, hasta puntos 2D de la ventana gráfica.
Ver matriz: la matriz de vista define la posición del ojo y la dirección de visualización en la escena.
Matriz modelo: la matriz modelo define la ubicación y el tamaño relativo de un objeto en la escena.
Después de que hayamos llenado las estructuras de datos anteriores con los datos correspondientes, tenemos que traducirlos a las matrices apropiadas. En el modo de compatibilidad OGL, esto se puede hacer con las funciones gluLookAt
y gluPerspective
que establecen los uniformes gl_ModelViewMatrix
, gl_NormalMatrix
, y gl_ModelViewProjectionMatrix
. En OGL 3.1 y GLSL #version 150 se eliminaron los uniformes incorporados, ya que toda la matriz de matriz de función fija quedó obsoleta. Si queremos usar el sombreador de alto nivel OGL con GLSL versión 330 o incluso más, tenemos que definir y establecer los uniformes de la matriz (aparte del uso de la palabra clave de compatibility
GLSL).
Configurar la perspectiva - Matriz de proyección.
Un punto en la ventana gráfica está visible cuando se encuentra en el AABB nativo (cuadro delimitador alineado con el eje) definido por los puntos (-1.0, -1.0, -1.0)
y (1.0, 1.0, 1.0)
. Esto se llama las Coordenadas del dispositivo normalizado (NDC). Un punto con las coordenadas (-1.0, -1.0, z)
se pintará en la esquina inferior izquierda de la ventana gráfica y un punto con las coordenadas (1.0, 1.0, z)
se pintará en la esquina superior derecha de la ventana gráfica. La coordenada Z se asigna desde el intervalo (-1.0, 1.0) al intervalo (0.0, 1.0) y se escribe en el Z-buffer.
Todo lo que podemos ver de la escena es dentro de una pirámide de 4 lados. La parte superior de la pirámide es la posición del ojo . Los 4 lados de la pirámide están definidos por el campo de vista ( fov_y
) y la relación de aspecto ( vp[0]/vp[1]
). La matriz de proyección debe asignar los puntos desde el interior de la pirámide al NDC definido por los puntos (-1.0, -1.0, -1.0)
y (1.0, 1.0, 1.0)
. En este punto, nuestra pirámide es infinita, no tiene un final en profundidad y no podemos mapear un espacio infinito a uno finito. Para esto, ahora necesitamos el plano cercano y el plano lejano , ya que transforman la pirámide en un tronco cortando la parte superior y limitando la pirámide en la profundidad. El plano cercano y el plano lejano deben elegirse de tal manera que incluyan todo lo que debería ser visible desde la escena.
El mapeo desde los puntos dentro de un frustum hasta el NDC es matemática pura y generalmente se puede resolver. El desarrollo de las fórmulas a menudo se discutió y se publicó repetidamente en toda la web. Ya que no puede insertar una fórmula LaTeX en una documentación de desbordamiento de pila, se prescinde aquí y solo se agrega el código fuente de C ++ y Python completado. Tenga en cuenta que las coordenadas de los ojos se definen en el sistema de coordenadas de la mano derecha, pero NDC utiliza el sistema de coordenadas de la mano izquierda. La matriz de proyección se calcula a partir de, el campo de visión fov_y
, la relación de aspecto vp[0]/vp[1]
, el plano near
y el plano far
.
C ++
using TVec4 = std::array< float, 4 >;
using TMat44 = std::array< TVec4, 4 >;
TMat44 Camera::Perspective( void )
{
float fn = far + near;
float f_n = far - near;
float r = (float)vp[0] / vp[1];
float t = 1.0f / tan( ToRad( fov_y ) / 2.0f );
return TMat44{
TVec4{ t / r, 0.0f, 0.0f, 0.0f },
TVec4{ 0.0f, t, 0.0f, 0.0f },
TVec4{ 0.0f, 0.0f, -fn / f_n, -1.0 },
TVec4{ 0.0f, 0.0f, -2.0f * far * near / f_n, 0.0f } };
}
Pitón
def Perspective(self):
fn, = self.far + self.near
f_n = self.far - self.near
r = self.vp[0] / self.vp[1]
t = 1 / math.tan( math.radians( self.fov_y ) / 2 )
return numpy.matrix( [
[ t/r, 0, 0, 0 ],
[ 0, t, 0, 0 ],
[ 0, 0, -fn/f_n, -1 ],
[ 0, 0, -2 * self.far * self.near / f_n, 0 ] ] )
Configurar la mirada a la escena - Ver matriz
En el sistema de coordenadas de la ventana gráfica, el eje Y apunta hacia arriba (0, 1, 0)
y el eje X apunta hacia la derecha (1, 0, 0)
. Esto da como resultado un eje Z que apunta hacia afuera de la ventana gráfica ( (0, 0, -1) = cross( X-axis, Y-axis )
).
En la escena, el eje X apunta hacia el este, el eje Y hacia el norte y el eje Z hacia la parte superior.
El eje X de la ventana gráfica (1, 0, 0)
coincide con el eje Y de la escena (1, 0, 0)
, el eje Y de la ventana gráfica (0, 1, 0 )
coincide con el eje Z de la escena (0, 0, 1)
y el eje Z de la ventana gráfica (0, 0, 1 )
coinciden con el eje Y negado de la escena (0, -1, 0)
.
Por lo tanto, cada punto y cada vector del sistema de referencia de la escena deben convertirse primero en coordenadas de la ventana gráfica. Esto se puede hacer mediante algunas operaciones de intercambio e inversión en los vectores escalares.
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
Para configurar una matriz de vista, la posición pos
, el objetivo target
y el vector up
deben asignarse al sistema de coordenadas de la ventana gráfica, como se describe anteriormente. Esto da los 2 puntos p
y t
y el vector u
, como en el siguiente fragmento de código. El eje Z de la matriz de vista es la línea de visión inversa, que se calcula mediante p - t
. El eje Y es el vector ascendente u
. El eje X se calcula por el producto cruzado del eje Y y el eje Z. Para la ortonormalización de la matriz de vista, el producto cruzado se usa por segunda vez, para calcular el eje Y desde el eje Z y el eje X (Por supuesto, la ortogonalización Gram-Schmidt funcionaría igual de bien). Al final, los 3 ejes deben estar normalizada y la posición del ojo pos
tiene que ser establecido como º origen de la matriz de vista.
El siguiente código define una matriz que encapsula exactamente los pasos necesarios para calcular una mirada a la escena:
- Convertir las coordenadas del modelo en coordenadas de la ventana gráfica.
- Gire en la dirección de la dirección de la vista.
- Movimiento a la posición del ojo.
C ++
template< typename T_VEC >
TVec3 Cross( T_VEC a, T_VEC b )
{
return { a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] };
}
template< typename T_A, typename T_B >
float Dot( T_A a, T_B b )
{
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
}
template< typename T_VEC >
void Normalize( T_VEC & v )
{
float len = sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] ); v[0] /= len; v[1] /= len; v[2] /= len;
}
TMat44 Camera::LookAt( void )
{
TVec3 mz = { pos[0] - target[0], pos[1] - target[1], pos[2] - target[2] };
Normalize( mz );
TVec3 my = { up[0], up[1], up[2] };
TVec3 mx = Cross( my, mz );
Normalize( mx );
my = Cross( mz, mx );
TMat44 v{
TVec4{ mx[0], my[0], mz[0], 0.0f },
TVec4{ mx[1], my[1], mz[1], 0.0f },
TVec4{ mx[2], my[2], mz[2], 0.0f },
TVec4{ Dot(mx, pos), Dot(my, pos), Dot(TVec3{-mz[0], -mz[1], -mz[2]}, pos), 1.0f }
};
return v;
}
pitón
def LookAt(self):
mz = Normalize( (self.pos[0]-self.target[0], self.pos[1]-self.target[1], self.pos[2]-self.target[2]) ) # inverse line of sight
mx = Normalize( Cross( self.up, mz ) )
my = Normalize( Cross( mz, mx ) )
tx = Dot( mx, self.pos )
ty = Dot( my, self.pos )
tz = Dot( (-mz[0], -mz[1], -mz[2]), self.pos )
return = numpy.matrix( [
[mx[0], my[0], mz[0], 0],
[mx[1], my[1], mz[1], 0],
[mx[2], my[2], mz[2], 0],
[tx, ty, tz, 1] ] )
Las matrices finalmente se escriben en uniformes y se usan en el sombreado de vértices para transformar las posiciones del modelo.
Sombreador de vértices
En el sombreado de vértices, se realiza una transformación después de la otra.
- La matriz del modelo lleva el objeto (malla) a su lugar en la escena. (Esto solo se enumera por estar completo y no se ha documentado aquí ya que no tiene nada que ver con la vista de la escena)
- La matriz de vista define la dirección desde la que se ve la escena. La transformación con la matriz de vista rota los objetos de la escena para que se vean desde la dirección de vista deseada con referencia al sistema de coordenadas de la ventana gráfica.
- La matriz de proyección transforma los objetos de una vista paralela en una vista en perspectiva.
#version 400
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inCol;
out vec3 vertCol;
uniform mat4 u_projectionMat44;
uniform mat4 u_viewMat44;
uniform mat4 u_modelMat44;
void main()
{
vertCol = inCol;
vec4 modelPos = u_modelMat44 * vec4( inPos, 1.0 );
vec4 viewPos = u_viewMat44 * modelPos;
gl_Position = u_projectionMat44 * viewPos;
}
Sombreador de fragmentos
El sombreador de fragmentos se enumera aquí solo por estar completo. El trabajo fue hecho antes.
#version 400
in vec3 vertCol;
out vec4 fragColor;
void main()
{
fragColor = vec4( vertCol, 1.0 );
}
Una vez que se compilan y gustan los sombreadores, las matrices pueden unirse a las variables uniformes.
C ++
int shaderProg = ;
Camera camera;
// ...
int prjMatLocation = glGetUniformLocation( shaderProg, "u_projectionMat44" );
int viewMatLocation = glGetUniformLocation( shaderProg, "u_viewMat44" );
glUniformMatrix4fv( prjMatLocation, 1, GL_FALSE, camera.Perspective().data()->data() );
glUniformMatrix4fv( viewMatLocation, 1, GL_FALSE, camera.LookAt().data()->data() );
Pitón
shaderProg =
camera = Camera()
# ...
prjMatLocation = glGetUniformLocation( shaderProg, b"u_projectionMat44" )
viewMatLocation = glGetUniformLocation( shaderProg, b"u_viewMat44" )
glUniformMatrix4fv( prjMatLocation, 1, GL_FALSE, camera.Perspective() )
glUniformMatrix4fv( viewMatLocation, 1, GL_FALSE, camera.LookAt() )
Además, he agregado el volcado de código completo de un ejemplo de Python (agregar el ejemplo de C ++ desafortunadamente superaría el límite de 30000 caracteres). En este ejemplo, la cámara se mueve elípticamente alrededor de un tetraedro en un punto focal de la elipse. La dirección de visualización siempre está dirigida al tetraeder.
Pitón
Para ejecutar el script de Python se debe instalar NumPy .
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
import numpy
from time import time
import math
import sys
def Cross( a, b ): return ( a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], 0.0 )
def Dot( a, b ): return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
def Normalize( v ):
len = math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] )
return (v[0] / len, v[1] / len, v[2] / len)
class Camera:
def __init__(self):
self.pos = (0, -8, 0)
self.target = (0, 0, 0)
self.up = (0, 0, 1)
self.fov_y = 90
self.vp = (800, 600)
self.near = 0.5
self.far = 100.0
def Perspective(self):
fn, f_n = self.far + self.near, self.far - self.near
r, t = self.vp[0] / self.vp[1], 1 / math.tan( math.radians( self.fov_y ) / 2 )
return numpy.matrix( [ [t/r,0,0,0], [0,t,0,0], [0,0,-fn/f_n,-1], [0,0,-2*self.far*self.near/f_n,0] ] )
def LookAt(self):
mz = Normalize( (self.pos[0]-self.target[0], self.pos[1]-self.target[1], self.pos[2]-self.target[2]) ) # inverse line of sight
mx = Normalize( Cross( self.up, mz ) )
my = Normalize( Cross( mz, mx ) )
tx = Dot( mx, self.pos )
ty = Dot( my, self.pos )
tz = Dot( (-mz[0], -mz[1], -mz[2]), self.pos )
return = numpy.matrix( [ [mx[0], my[0], mz[0], 0], [mx[1], my[1], mz[1], 0], [mx[2], my[2], mz[2], 0], [tx, ty, tz, 1] ] )
# shader program object
class ShaderProgram:
def __init__( self, shaderList, uniformNames ):
shaderObjs = []
for sh_info in shaderList: shaderObjs.append( self.CompileShader(sh_info[0], sh_info[1] ) )
self.LinkProgram( shaderObjs )
self.__unifomLocation = {}
for name in uniformNames:
self.__unifomLocation[name] = glGetUniformLocation( self.__prog, name )
print( "uniform %-30s at loaction %d" % (name, self.__unifomLocation[name]) )
def Use(self):
glUseProgram( self.__prog )
def SetUniformMat44( self, name, mat ):
glUniformMatrix4fv( self.__unifomLocation[name], 1, GL_FALSE, mat )
# read shader program and compile shader
def CompileShader(self, sourceFileName, shaderStage):
with open( sourceFileName, 'r' ) as sourceFile:
sourceCode = sourceFile.read()
nameMap = { GL_VERTEX_SHADER: 'vertex', GL_FRAGMENT_SHADER: 'fragment' }
print( '\n%s shader code:' % nameMap.get( shaderStage, '' ) )
print( sourceCode )
shaderObj = glCreateShader( shaderStage )
glShaderSource( shaderObj, sourceCode )
glCompileShader( shaderObj )
result = glGetShaderiv( shaderObj, GL_COMPILE_STATUS )
if not (result):
print( glGetShaderInfoLog( shaderObj ) )
sys.exit()
return shaderObj
# linke shader objects to shader program
def LinkProgram(self, shaderObjs):
self.__prog = glCreateProgram()
for shObj in shaderObjs: glAttachShader( self.__prog, shObj )
glLinkProgram( self.__prog )
result = glGetProgramiv( self.__prog, GL_LINK_STATUS )
if not ( result ):
print( 'link error:' )
print( glGetProgramInfoLog( self.__prog ) )
sys.exit()
# vertex array object
class VAObject:
def __init__( self, dataArrays, tetIndices ):
self.__obj = glGenVertexArrays( 1 )
self.__noOfIndices = len( tetIndices )
self.__indexArr = numpy.array( tetIndices, dtype='uint' )
noOfBuffers = len( dataArrays )
buffers = glGenBuffers( noOfBuffers )
glBindVertexArray( self.__obj )
for i_buffer in range( 0, noOfBuffers ):
vertexSize, dataArr = dataArrays[i_buffer]
glBindBuffer( GL_ARRAY_BUFFER, buffers[i_buffer] )
glBufferData( GL_ARRAY_BUFFER, numpy.array( dataArr, dtype='float32' ), GL_STATIC_DRAW )
glEnableVertexAttribArray( i_buffer )
glVertexAttribPointer( i_buffer, vertexSize, GL_FLOAT, GL_FALSE, 0, None )
def Draw(self):
glBindVertexArray( self.__obj )
glDrawElements( GL_TRIANGLES, self.__noOfIndices, GL_UNSIGNED_INT, self.__indexArr )
# glut window
class Window:
def __init__( self, cx, cy ):
self.__vpsize = ( cx, cy )
glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH )
glutInitWindowPosition( 0, 0 )
glutInitWindowSize( self.__vpsize[0], self.__vpsize[1] )
self.__id = glutCreateWindow( b'OGL window' )
glutDisplayFunc( self.OnDraw )
glutIdleFunc( self.OnDraw )
def Run( self ):
self.__startTime = time()
glutMainLoop()
# draw event
def OnDraw(self):
self.__vpsize = ( glutGet( GLUT_WINDOW_WIDTH ), glutGet( GLUT_WINDOW_HEIGHT ) )
currentTime = time()
# set up camera
camera = Camera()
camera.vp = self.__vpsize
camera.pos = self.EllipticalPosition( 7, 4, self.CalcAng( currentTime, 10 ) )
# set up attributes and shader program
glEnable( GL_DEPTH_TEST )
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT )
prog.Use()
prog.SetUniformMat44( b"u_projectionMat44", camera.Perspective() )
prog.SetUniformMat44( b"u_viewMat44", camera.LookAt() )
# draw object
modelMat = numpy.matrix(numpy.identity(4), copy=False, dtype='float32')
prog.SetUniformMat44( b"u_modelMat44", modelMat )
tetVAO.Draw()
glutSwapBuffers()
def Fract( self, val ): return val - math.trunc(val)
def CalcAng( self, currentTime, intervall ): return self.Fract( (currentTime - self.__startTime) / intervall ) * 2.0 * math.pi
def CalcMove( self, currentTime, intervall, range ):
pos = self.Fract( (currentTime - self.__startTime) / intervall ) * 2.0
pos = pos if pos < 1.0 else (2.0-pos)
return range[0] + (range[1] - range[0]) * pos
def EllipticalPosition( self, a, b, angRag ):
a_b = a * a - b * b
ea = 0 if (a_b <= 0) else math.sqrt( a_b )
eb = 0 if (a_b >= 0) else math.sqrt( -a_b )
return ( a * math.sin( angRag ) - ea, b * math.cos( angRag ) - eb, 0 )
# initialize glut
glutInit()
# create window
wnd = Window( 800, 600 )
# define tetrahedron vertex array opject
sin120 = 0.8660254
tetVAO = VAObject(
[ (3, [ 0.0, 0.0, 1.0, 0.0, -sin120, -0.5, sin120 * sin120, 0.5 * sin120, -0.5, -sin120 * sin120, 0.5 * sin120, -0.5 ]),
(3, [ 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, ])
],
[ 0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2 ]
)
# load, compile and link shader
prog = ShaderProgram(
[ ('python/ogl4camera/camera.vert', GL_VERTEX_SHADER),
('python/ogl4camera/camera.frag', GL_FRAGMENT_SHADER)
],
[b"u_projectionMat44", b"u_viewMat44", b"u_modelMat44"] )
# start main loop
wnd.Run()