opengl
OGL-weergave en projectie
Zoeken…
Invoering
Over modelmatrix, beeldmatrix, orthografische- en perspectiefprojectie
Implementeer een camera in OGL 4.0 GLSL 400
Als we naar een scène willen kijken alsof we het met een camera hadden gefotografeerd, moeten we eerst een aantal dingen definiëren:
- De positie van waaruit de scène wordt bekeken, de oogpositie
pos
. - Het punt waar we naar kijken in de scène (
target
). Het is ook gebruikelijk om de richting te bepalen waarin we kijken. Technisch gezien hebben we een zichtlijn nodig . Eén rechte in de ruimte wordt wiskundig gedefinieerd door 2 punten of door een punt en een vector. Het eerste deel van de definitie is de oogpositie en het tweede deel is hettarget
of de gezichtslijnlos
. - Richting opwaarts
up
. - Het gezichtsveld '
fov_y
. Dit betekent de hoek tussen de twee rechte lijnen, beginnend bij de oogpositie en eindigend op het meest linkse en het meest rechtse punt, die tegelijkertijd kan worden gezien. - De grote en de beeldverhouding van de viewport waarop we onze afbeelding
vp
projecteren. - Het nabije vliegtuig
near
en het verre vliegtuigfar
. Het nabije vlak is de afstand van de oogpositie tot het vlak van waaruit de objecten voor ons zichtbaar worden. Het verre vlak is de afstand van de oogpositie tot het vlak waarop de objecten van de scène voor ons zichtbaar zijn. Een uitleg over wat het nabije vliegtuig en het verre vliegtuig nodig zijn, volgt later ..
Een definitie van deze gegevens in C ++ en in Python kan er zo uitzien:
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};
};
Python
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
Om al deze informatie in overweging te nemen bij het tekenen van een scène, worden meestal een projectiematrix en een beeldmatrix gebruikt. Om de afzonderlijke delen van een scène in de scène te rangschikken, worden modelmatrices gebruikt. Deze worden hier echter alleen voor de volledigheid vermeld en worden hier niet behandeld.
Projectiematrix: de projectiematrix beschrijft de mapping van 3D-punten in de wereld zoals deze worden gezien vanaf een pinhole-camera, tot 2D-punten van het kijkvenster.
Beeldmatrix: de beeldmatrix bepaalt de oogpositie en de kijkrichting in de scène.
Modelmatrix: de modelmatrix definieert de locatie en de relatieve grootte van een object in de scène.
Nadat we de datastructuren hierboven hebben gevuld met de bijbehorende gegevens, moeten we deze vertalen in de juiste matrices. In de OGL-compatibiliteitsmodus kan dit worden gedaan met de functies gluLookAt
en gluPerspective
die de ingebouwde uniformen gl_ModelViewMatrix
, gl_NormalMatrix
en gl_ModelViewProjectionMatrix
. In OGL 3.1 en GLSL #version 150 zijn de ingebouwde uniformen verwijderd, omdat de gehele matrix met vaste functies is verouderd. Als we OGL high-level shader met GLSL-versie 330 of zelfs hoger willen gebruiken, moeten we zelf de matrixuniformen definiëren en instellen (Afgezien van het gebruik van het GLSL- compatibility
).
Stel het perspectief in - Projectiematrix
Een punt op het kijkvenster is zichtbaar wanneer het zich in de oorspronkelijke AABB (asuitlijnend selectiekader) bevindt, gedefinieerd door de punten (-1.0, -1.0, -1.0)
en (1.0, 1.0, 1.0)
. Dit wordt de genormaliseerde apparaatcoördinaten (NDC) genoemd. Een punt met de coördinaten (-1.0, -1.0, z)
wordt in de linkeronderhoek van het kijkvenster geschilderd en een punt met de coördinaten (1.0, 1.0, z)
wordt in de rechterbovenhoek van het kijkvenster geschilderd. De Z-coördinaat wordt toegewezen van het interval (-1.0, 1.0) tot het interval (0.0, 1.0) en in de Z-buffer geschreven.
Alles wat we kunnen zien in de scène is binnen een 4-zijdige piramide. De bovenkant van de piramide is de oogpositie . De 4 zijden van de piramide worden bepaald door het fov_y
( fov_y
) en de beeldverhouding ( vp[0]/vp[1]
). De projectiematrix moet de punten van binnen de piramide toewijzen aan de NDC gedefinieerd door de punten (-1.0, -1.0, -1.0)
en (1.0, 1.0, 1.0)
. Op dit punt is onze piramide oneindig, het heeft geen einde in de diepte en we kunnen een oneindige ruimte niet in een eindige ruimte plaatsen. Hiervoor hebben we nu het nabije vlak en het verre vlak nodig , ze transformeren de piramide in een afgeknotte koepel door de bovenkant te snijden en de piramide in de diepte te beperken. Het nabije vlak en het verre vlak moeten zo worden gekozen dat ze alles bevatten wat zichtbaar moet zijn vanuit de scène.
Het in kaart brengen van de punten binnen een frustum naar de NDC is pure wiskunde en kan in het algemeen worden opgelost. De ontwikkeling van de formules werd vaak besproken en herhaaldelijk gepubliceerd op internet. Aangezien u geen LaTeX-formule in een Stack Overflow-documentatie kunt invoegen, wordt hier afgezien en wordt alleen de voltooide C ++ en Python-broncode toegevoegd. Merk op dat de oogcoördinaten zijn gedefinieerd in het rechtshandige coördinatensysteem, maar NDC gebruikt het linkshandige coördinatensysteem. De projectiematrix wordt berekend uit het gezichtsveld fov_y
, de beeldverhouding vp[0]/vp[1]
, het nabije vlak near
en het verre vlak 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 } };
}
Python
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 ] ] )
Stel het uiterlijk van de scène in - Bekijk matrix
In het coördinatensysteem op het kijkvenster wijst de Y-as naar boven (0, 1, 0)
en wijst de X-as naar rechts (1, 0, 0)
. Dit resulteert in een Z-as die uit de viewport wijst ( (0, 0, -1) = cross( X-axis, Y-axis )
).
In de scène wijst de X-as naar het oosten, de Y-as naar het noorden en de Z-as naar de top.
De X-as van de viewport (1, 0, 0)
overeen met de Y-as van de scène (1, 0, 0)
, de Y-as van de viewport (0, 1, 0 )
overeen met de Z-as van de scène (0, 0, 1)
en de Z-as van de viewport (0, 0, 1 )
overeen met de ontkende Y-as van de scène (0, -1, 0)
.
Elk punt en elke vector uit het referentiesysteem van de scène moet daarom eerst worden omgezet in viewport-coördinaten. Dit kan worden gedaan door enkele swap- en omkeringsbewerkingen in de scalaire vectoren.
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
Het inrichten van een matrix aanzicht de positie pos
het doelwit target
en de vector up
moeten worden toegewezen aan het kijkvenster coördinatenstelsel, zoals hierboven beschreven. Dit geeft de 2 punten p
en t
en de vector u
, zoals in het volgende codefragment. De Z-as van de beeldmatrix is de inverse zichtlijn, die wordt berekend door p - t
. De Y-as is de omhooggaande vector u
. De X-as wordt berekend door het kruisproduct van de Y-as en de Z-as. Voor het orthonormaliseren van de beeldmatrix wordt het kruisproduct een tweede keer gebruikt om de Y-as te berekenen vanaf de Z-as en de X-as (natuurlijk zou de Gram-Schmidt-orthogonalisatie net zo goed werken). Na afloop moeten alle 3 assen worden genormaliseerd en de oogpositie pos
moet worden ingesteld als e oorsprong van de weergave matrix.
De onderstaande code definieert een matrix die precies de stappen bevat die nodig zijn om een kijkje in de scène te berekenen:
- Modelcoördinaten omzetten in viewport-coördinaten.
- Roteer in de richting van de kijkrichting.
- Beweging naar de oogpositie
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;
}
Python
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] ] )
De matrices worden uiteindelijk in uniformen geschreven en in de hoekpuntshader gebruikt om de modelposities te transformeren.
Vertex shader
In de hoekpuntshader wordt de ene transformatie na de andere uitgevoerd.
- De modelmatrix brengt het object (mesh) op zijn plaats in de scène. (Dit wordt alleen vermeld voor de volledigheid en is hier niet gedocumenteerd omdat het niets te maken heeft met het uitzicht op de scène)
- De weergavematrix definieert de richting van waaruit de scène wordt bekeken. De transformatie met de beeldmatrix roteert de objecten van de scène zodat ze worden bekeken vanuit de gewenste kijkrichting met verwijzing naar het coördinatensysteem van de viewport.
- De projectiematrix transformeert de objecten van een parallelle weergave in een perspectiefweergave.
#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
De fragment-arcering wordt hier alleen voor de volledigheid vermeld. Het werk was eerder gedaan.
#version 400
in vec3 vertCol;
out vec4 fragColor;
void main()
{
fragColor = vec4( vertCol, 1.0 );
}
Nadat de arcering is gecompileerd en geliked, kunnen de matrices worden gebonden aan de uniforme variabelen.
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() );
Python
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() )
Bovendien heb ik de volledige codedump van een Python-voorbeeld toegevoegd (het C ++ -voorbeeld toevoegen zou helaas de limiet van 30000 tekens overschrijden). In dit voorbeeld beweegt de camera elliptisch rond een tetraëder op een brandpunt van de ellips. De kijkrichting is altijd gericht op de tetraeder.
Python
Om het Python-script uit te voeren, moet NumPy zijn geïnstalleerd.
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()