opengl
Vista OGL e proiezione
Ricerca…
introduzione
Informazioni sulla matrice del modello, sulla matrice di visualizzazione, sulla proiezione ortogonale e prospettica
Implementare una fotocamera in OGL 4.0 GLSL 400
Se vogliamo guardare una scena come se l'avessimo fotografata con una fotocamera, dobbiamo prima definire alcune cose:
- La posizione da cui viene vista la scena, la posizione dell'occhio
pos
. - Il punto che guardiamo nella scena (
target
). È anche comune definire la direzione in cui guardiamo. Tecnicamente abbiamo bisogno di una linea di vista . Una retta nello spazio è definita matematicamente da 2 punti o da un punto e un vettore. La prima parte della definizione è la posizione dell'occhio e il 2o è iltarget
o il vettore della linea di vistalos
. - La direzione verso l'
up
. - Il campo visivo '
fov_y
. Ciò significa l'angolo tra le due linee rette, che inizia dalla posizione dell'occhio e termina nel punto più a sinistra e nel punto più a destra, che può essere visto contemporaneamente. - Il grande e il rapporto di aspetto del viewport a cui proiettiamo la nostra immagine
vp
. - L' aereo
near
e l' aereofar
. Il piano vicino è la distanza dalla posizione dell'occhio al piano da cui gli oggetti diventano visibili a noi. Il piano lontano è la distanza dalla posizione dell'occhio al piano a cui gli oggetti della scena sono visibili. Una spiegazione di ciò che il piano vicino e il piano lontano sono necessari seguirà più tardi ..
Una definizione di questi dati in C ++ e in Python può apparire come questa:
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};
};
Pitone
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
Per tenere tutte queste informazioni in considerazione quando si disegna una scena, vengono solitamente utilizzate una matrice di proiezione e una matrice di viste. Per organizzare le singole parti di una scena nella scena, vengono utilizzate le matrici dei modelli. Tuttavia, questi sono menzionati qui solo per motivi di completezza e non saranno trattati qui.
Matrice di proiezione: la matrice di proiezione descrive la mappatura da punti 3D nel mondo così come sono visti da una camera stenopeica, a punti 2D della vista.
Visualizza matrice: la matrice di visualizzazione definisce la posizione dell'occhio e la direzione di visione sulla scena.
Matrice modello: la matrice modello definisce la posizione e la dimensione relativa di un oggetto nella scena.
Dopo aver riempito le strutture di dati sopra con i dati corrispondenti, dobbiamo tradurli nelle matrici appropriate. Nella modalità di compatibilità OGL, questo può essere fatto con le funzioni gluLookAt
e gluPerspective
che impostano le uniformi gl_ModelViewMatrix
, gl_NormalMatrix
e gl_ModelViewProjectionMatrix
. In OGL 3.1 e GLSL #version 150 le uniformi incorporate sono state rimosse, perché l'intero stack matrix a funzione fissa è diventato deprecato. Se vogliamo utilizzare OGL ad alto livello con GLSL versione 330 o anche superiore, dobbiamo definire e impostare noi stessi le uniformi di matrice (a parte l'uso della parola chiave di compatibility
GLSL).
Imposta la prospettiva - Matrice di proiezione
Un punto sul viewport è visibile quando si trova nell'AABB nativo (bounding box allineato asse) definito dai punti (-1.0, -1.0, -1.0)
e (1.0, 1.0, 1.0)
. Questo è chiamato Normalised Device Coordinates (NDC). Un punto con le coordinate (-1.0, -1.0, z)
verrà dipinto nell'angolo in basso a sinistra della vista e un punto con le coordinate (1.0, 1.0, z)
verrà dipinto nell'angolo superiore destro della finestra. La coordinata Z viene mappata dall'intervallo (-1.0, 1.0) all'intervallo (0.0, 1.0) e scritta nel buffer Z.
Tutto ciò che possiamo vedere dalla scena è all'interno di una piramide a 4 lati. La parte superiore della piramide è la posizione dell'occhio . I 4 lati della piramide sono definiti dal campo visivo ( fov_y
) e dal rapporto aspetto ( vp[0]/vp[1]
). La matrice di proiezione deve mappare i punti dall'interno della piramide all'NDC definito dai punti (-1.0, -1.0, -1.0)
e (1.0, 1.0, 1.0)
. A questo punto la nostra piramide è infinita, non ha fine in profondità e non possiamo mappare uno spazio infinito in uno finito. Per questo ora abbiamo bisogno del piano vicino e del piano lontano , trasformano la piramide in un tronco tagliando la cima e limitando la piramide in profondità. Il piano vicino e il piano lontano devono essere scelti in modo tale da includere tutto ciò che dovrebbe essere visibile dalla scena.
La mappatura dai punti all'interno di un tronco all'NDC è pura matematica e può essere generalmente risolta. Lo sviluppo delle formule è stato spesso discusso e pubblicato più volte su tutto il web. Dal momento che non è possibile inserire una formula LaTeX in una documentazione di Stack Overflow, qui non è disponibile e solo il codice sorgente C ++ e Python completato viene aggiunto. Notare che le coordinate dell'occhio sono definite nel sistema di coordinate della mano destra, ma NDC usa il sistema di coordinate della mano sinistra. La matrice di proiezione viene calcolata da, il campo di vista fov_y
, il rapporto di aspetto vp[0]/vp[1]
, il piano near
e il lontano piano 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 } };
}
Pitone
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 ] ] )
Imposta l'aspetto della scena: Visualizza matrice
Nel sistema di coordinate sulla vista, l'asse Y punta verso l'alto (0, 1, 0)
e l'asse X punta a destra (1, 0, 0)
. Ne risulta un asse Z che indica fuori dalla vista ( (0, 0, -1) = cross( X-axis, Y-axis )
).
Nella scena, l'asse X punta verso est, l'asse Y verso nord e l'asse Z verso l'alto.
L'asse X della vista (1, 0, 0)
corrisponde all'asse Y della scena (1, 0, 0)
, l'asse Y della vista (0, 1, 0 )
corrisponde all'asse Z della scena (0, 0, 1)
e l'asse Z della vista (0, 0, 1 )
corrisponde all'asse Y negato della scena (0, -1, 0)
.
Pertanto, ogni punto e ogni vettore dal sistema di riferimento della scena devono essere convertiti prima in coordinate di vista. Questo può essere fatto scambiando e invertendo le operazioni nei vettori scalari.
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
Per configurare una matrice di visualizzazione, la posizione pos
, il target di target
e il vettore up
devono essere mappati nel sistema di coordinate del viewport, come descritto sopra. Ciò fornisce i 2 punti p
e t
e il vettore u
, come nel seguente frammento di codice. L'asse Z della matrice della vista è la linea di vista inversa, che è calcolata da p - t
. L'asse Y è il vettore up u
. L'asse X viene calcolato dal prodotto incrociato dell'asse Y e dell'asse Z. Per ortonormalizzare la matrice della vista, il prodotto incrociato viene utilizzato una seconda volta, per calcolare l'asse Y dall'asse Z e l'asse X (naturalmente l'ortogonalizzazione Gram-Schmidt funzionerebbe altrettanto bene). Alla fine, tutti i 3 assi devono essere normalizzati e la posizione dell'occhio pos
deve essere impostato come th origine della matrice vista.
Il codice sottostante definisce una matrice che racchiude esattamente i passaggi necessari per calcolare un aspetto della scena:
- Conversione delle coordinate del modello in coordinate del viewport.
- Ruota nella direzione della direzione della vista.
- Movimento alla posizione dell'occhio
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;
}
pitone
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] ] )
Le matrici vengono infine scritte in uniformi e utilizzate nel vertex shader per trasformare le posizioni del modello.
Vertex shader
Nel vertex shader, viene eseguita una trasformazione dopo l'altra.
- La matrice del modello porta l'oggetto (mesh) nella sua posizione nella scena. (Questo è elencato solo per motivi di completezza e non è stato documentato qui poiché non ha nulla a che fare con la vista della scena)
- La matrice della vista definisce la direzione da cui viene vista la scena. La trasformazione con la matrice della vista ruota gli oggetti della scena in modo che vengano visualizzati dalla direzione di visualizzazione desiderata con riferimento al sistema di coordinate della finestra.
- La matrice di proiezione trasforma gli oggetti da una vista parallela in una vista prospettica.
#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;
}
Fragment shader
Lo shader di frammenti è elencato qui solo per motivi di completezza. Il lavoro è stato fatto prima.
#version 400
in vec3 vertCol;
out vec4 fragColor;
void main()
{
fragColor = vec4( vertCol, 1.0 );
}
Dopo che lo shader è stato compilato e apprezzato, le matrici possono essere associate alle variabili uniformi.
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() );
Pitone
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() )
Inoltre, ho aggiunto l'intero dump del codice di un esempio Python (per aggiungere l'esempio C ++, sfortunatamente, supererei il limite di 30000 caratteri). In questo esempio, la telecamera si muove in modo ellittico attorno a un tetraedro in corrispondenza di un punto focale dell'ellisse. La direzione di visione è sempre diretta al tetraeder.
Pitone
Per eseguire lo script Python deve essere installato 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()