opengl
OGL-vy och projektion
Sök…
Introduktion
Om modellmatris, visa matris, ortografisk och perspektivprojektion
Implementera en kamera i OGL 4.0 GLSL 400
Om vi vill titta på en scen som om vi fotograferade den med en kamera måste vi först definiera några saker:
- Positionen från vilken scenen visas, ögons position
pos
. - Den punkt vi tittar på i scenen (
target
). Det är också vanligt att definiera den riktning vi ser in i. Tekniskt behöver vi en siktlinje . En rak i rymden definieras matematiskt antingen med 2 poäng eller av en punkt och en vektor. Den första delen av definitionen är ögonpositionen och den andra är antingentarget
eller siktlinjen vektorlos
. - Riktningen uppåt
up
. - Synfältet '
fov_y
. Detta betyder vinkeln mellan de två raka linjerna, börjar vid ögonpositionen och slutar vid den vänstra punkten och den högra punkten, som kan ses samtidigt. - Det stora och bildförhållandet på visningsområdet som vi projicerar vår bild
vp
. - Nära plan
near
och långt planfar
. Nära planet är avståndet från ögonläget till planet där föremålen blir synliga för oss. Det avlägsna planet är avståndet från ögonläget till det planet som scenens objekt är synliga för oss. En förklaring av vad det närmaste planet och fjärrplanet behövs följer senare ..
En definition av dessa data i C ++ och i Python kan se ut så här:
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};
};
Pytonorm
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
För att ta hänsyn till all denna information vid ritning av en scen används vanligen en projektionsmatris och en visningsmatris. För att ordna de enskilda delarna av en scen i scenen används modellmatriser. Dessa nämns här endast för fullständighetens skull och kommer inte att behandlas här.
Projektionsmatris: Projektionsmatrisen beskriver kartläggningen från 3D-punkter i världen som de ses från en pinhole-kamera, till 2D-punkter i visningsområdet.
Visa matris: Visningsmatrisen definierar ögonpositionen och tittningsriktningen på scenen.
Modellmatris: Modellmatrisen definierar platsen och den relativa storleken på ett objekt i scenen.
När vi har fyllt datastrukturerna ovan med motsvarande data måste vi översätta dem till lämpliga matriser. I OGL-kompatibilitetsläget kan detta göras med gluLookAt
och gluPerspective
som ställer in de inbyggda uniformerna gl_ModelViewMatrix
, gl_NormalMatrix
och gl_ModelViewProjectionMatrix
. I OGL 3.1 och GLSL #version 150 togs de inbyggda uniformerna bort eftersom hela matrisstacken med fast funktion försvann. Om vi vill använda OGL-skuggning på hög nivå med GLSL-version 330 eller ännu högre måste vi definiera och ställa in matrisuniformerna för oss (Bortsett från användningen av GLSL- compatibility
).
Ställ in perspektivet - Projektionsmatris
En punkt på visningsområdet är synlig när den är i den ursprungliga AABB (axelinriktad avgränsningsruta) definierad av punkterna (-1.0, -1.0, -1.0)
och (1.0, 1.0, 1.0)
. Detta kallas NDC (Normalised Device Coordinates). En punkt med koordinaterna (-1.0, -1.0, z)
målas till utsiktsportens nedre vänstra hörn och en punkt med koordinaterna (1.0, 1.0, z)
målas till det övre högra hörnet av utsiktsporten. Z-koordinaten mappas från intervallet (-1,0, 1,0) till intervallet (0,0, 1,0) och skrivs in i Z-bufferten.
Allt vi kan se från scenen ligger inom en 4-sidig pyramid. Överst på pyramiden är ögons position . De fyra sidorna av pyramiden definieras av fov_y
fil ( fov_y
) och bildförhållandet ( vp[0]/vp[1]
). Projektionsmatrisen måste kartlägga punkterna från insidan av pyramiden till NDC som definieras av punkterna (-1.0, -1.0, -1.0)
och (1.0, 1.0, 1.0)
. Vid denna punkt är vår pyramid oändlig, den har inget slut på djupet och vi kan inte kartlägga ett oändligt utrymme till ett ändligt. För detta behöver vi nu det nära planet och det fjärde planet , de förvandlar pyramiden till en frustum genom att klippa toppen och begränsa pyramiden i djupet. Det nära planet och det bortre planet måste väljas på ett sådant sätt att de inkluderar allt som borde vara synligt från scenen.
Kartläggningen från punkterna inom en stympning till NDC är ren matematik och kan i allmänhet lösas. Utvecklingen av formlerna diskuterades ofta och publicerades upprepade gånger på webben. Eftersom du inte kan infoga en LaTeX-formel i en dokumentöverskridningsdokumentation kommer detta att undvikas här och endast den färdiga C ++ och Python-källkoden läggs till. Observera att ögonkoordinaterna är definierade i det högerhänta koordinatsystemet, men NDC använder det vänsterhänta koordinatsystemet. Utsprånget matris beräknas från, synfältet fov_y
, sidförhållandet vp[0]/vp[1]
, en nära planet near
och långt planet 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 } };
}
Pytonorm
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 ] ] )
Ställ in blicken på scenen - Visa matris
I koordinatsystemet på visningsområdet pekar Y-axeln uppåt (0, 1, 0)
och X-axeln pekar till höger (1, 0, 0)
. Detta resulterar i en Z-axel som pekar ut ur visningsområdet ( (0, 0, -1) = cross( X-axis, Y-axis )
).
I scenen pekar X-axeln mot öster, Y-axeln mot norr och Z-axeln upptill.
Visningsportens X-axel (1, 0, 0)
matchar scenens Y-axel (1, 0, 0)
, visningsportens Y-axel (0, 1, 0 )
matchar scenens Z-axel (0, 0, 1)
och Z-axeln för visningsområdet (0, 0, 1 )
matchar den negerade Y-axeln för scenen (0, -1, 0)
.
Varje punkt och varje vektor från scenens referenssystem måste därför först konverteras till visningskoordinater. Detta kan göras genom att byta och invertera operationer i de skalära vektorerna.
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
Att installera en vy matris positions pos
, mål- target
och upp vektorn up
måste att avbildas i visningskoordinatsystemet, såsom beskrivits ovan. Detta ger de 2 punkterna p
och t
och vektorn u
, som i följande kodavsnitt. Z-axeln för vymatrisen är den omvända siktlinjen, som beräknas med p - t
. Y-axeln är uppvektorn u
. X-axeln beräknas av tvärprodukten av Y-axeln och Z-axeln. För orthonormalisering av visningsmatrisen används tvärprodukten en andra gång för att beräkna Y-axeln från Z-axeln och X-axeln (Naturligtvis skulle Gram-Schmidt-ortogonaliseringen fungera lika bra). I slutet, måste alla 3 axlar normalis och ögats läge pos
måste ställas in som e ursprung av vyn matrisen.
Koden nedan definierar en matris som exakt innehåller de steg som krävs för att beräkna en titt på scenen:
- Konvertera modellkoordinater till visningskoordinater.
- Rotera i riktning mot siktriktningen.
- Rörelse till ögons position
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;
}
pytonorm
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] ] )
Matriserna skrivs slutligen i uniformer och används i toppskärmaren för att transformera modellpositionerna.
Korsskärmar
I toppskyddet utförs en transformation efter den andra.
- Modellmatrisen tar objektet (mesh) till sin plats i scenen. (Detta listas endast för fullständighetens skull och har inte dokumenterats här eftersom det inte har något att göra med vyn till scenen)
- Visningsmatrisen definierar riktningen från vilken scenen visas. Transformationen med utsiktsmatrisen roterar scenens objekt så att de ses från den önskade siktriktningen med hänvisning till koordinatsystemet i visningsområdet.
- Projektionsmatrisen omvandlar objekten från en parallellvy till en perspektivvy.
#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
Fragmentet shader listas här endast för fullständighetens skull. Arbetet gjordes tidigare.
#version 400
in vec3 vertCol;
out vec4 fragColor;
void main()
{
fragColor = vec4( vertCol, 1.0 );
}
Efter att skuggningen har sammanställts och gillades kan matriserna binda till de enhetliga variablerna.
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() );
Pytonorm
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() )
Dessutom har jag lagt till hela koddumpen i ett Python-exempel (Att lägga till C ++ -exemplet skulle tyvärr överskrida gränsen på 30000 tecken). I det här exemplet rör sig kameran elliptiskt runt en tetrahedron vid ellipsens fokuspunkt. Visningsriktningen riktas alltid till tetraederen.
Pytonorm
För att köra Python-skriptet måste NumPy vara installerad.
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()