opengl
Widok i projekcja OGL
Szukaj…
Wprowadzenie
Informacje o matrycy modelu, matrycy widoku, rzucie prostokątnym i perspektywicznym
Zaimplementuj kamerę w OGL 4.0 GLSL 400
Jeśli chcemy spojrzeć na scenę tak, jakbyśmy sfotografowali ją aparatem, musimy najpierw zdefiniować kilka rzeczy:
- Pozycja, z której oglądana jest scena, pozycja oka
pos
. - Punkt, na który patrzymy na scenie (
target
). Często określa się również kierunek, w którym patrzymy. Technicznie potrzebujemy linii wzroku . Jedna prosta w przestrzeni jest matematycznie zdefiniowana albo przez 2 punkty, albo przez punkt i wektor. Pierwsza część definicji jest pozycja oko i 2nd jest albotarget
lub linii wzroku wektoralos
. - Kierunek w górę w
up
. - Pole widzenia „
fov_y
. Oznacza to kąt między dwiema liniami prostymi, zaczynający się w pozycji oka i kończący się w punkcie skrajnie lewym i skrajnym prawym, który można zobaczyć jednocześnie. - Duży i proporcje rzutni, do której
vp
nasz obrazvp
. - Bliski samolot
near
i daleki samolotfar
. Bliska płaszczyzna to odległość od pozycji oka do płaszczyzny, z której obiekty stają się dla nas widoczne. Płaszczyzna najdalsza to odległość od pozycji oka do płaszczyzny, do której obiekty sceny są dla nas widoczne. Wyjaśnienie, co jest potrzebne do bliskiej i dalekiej płaszczyzny , nastąpi później.
Definicja tych danych w C ++ i Python może wyglądać następująco:
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};
};
Pyton
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
Aby wziąć pod uwagę wszystkie te informacje podczas rysowania sceny, zwykle stosuje się macierz projekcji i macierz widoku. W celu ułożenia poszczególnych części sceny na scenie stosuje się macierze modelowe. Zostały one jednak wymienione tutaj wyłącznie w celu zapewnienia kompletności i nie zostaną tutaj omówione.
Macierz projekcji: Macierz projekcji opisuje odwzorowanie punktów 3D na świecie widzianych z kamery otworkowej na punkty 2D rzutni.
Macierz widoków: Macierz widoków określa pozycję oka i kierunek patrzenia na scenę.
Macierz modelu: Macierz modelu określa położenie i względny rozmiar obiektu w scenie.
Po wypełnieniu powyższych struktur danych odpowiednimi danymi musimy je przetłumaczyć na odpowiednie macierze. W trybie zgodności z OGL można tego dokonać za pomocą funkcji gluLookAt
i gluPerspective
które ustawiają wbudowane uniformy gl_ModelViewMatrix
, gl_NormalMatrix
i gl_ModelViewProjectionMatrix
. W OGL 3.1 i GLSL #version 150 wbudowane mundury zostały usunięte, ponieważ cały stos macierzy o stałej funkcji stał się przestarzały. Jeśli chcemy korzystać z modułu cieniującego wysokiego poziomu OGL z wersją GLSL 330 lub nawet wyższą, musimy zdefiniować i ustawić sobie uniformy macierzy (oprócz użycia słowa kluczowego compatibility
GLSL).
Ustaw perspektywę - Matryca projekcji
Punkt w rzutni jest widoczny, gdy znajduje się w natywnym AABB (obwiednia wyrównana do osi) zdefiniowanym przez punkty (-1.0, -1.0, -1.0)
i (1.0, 1.0, 1.0)
. Nazywa się to znormalizowanymi współrzędnymi urządzenia (NDC). Punkt o współrzędnych (-1.0, -1.0, z)
zostanie namalowany w lewym dolnym rogu rzutni, a punkt o współrzędnych (1.0, 1.0, z)
zostanie namalowany w prawym górnym rogu rzutni. Współrzędna Z jest odwzorowywana z przedziału (-1,0; 1,0) na przedział (0,0; 1,0) i zapisywana w buforze Z.
Wszystko, co możemy zobaczyć ze sceny, znajduje się w 4-stronnej piramidzie. Szczyt piramidy to pozycja oka . 4 strony piramidy są zdefiniowane przez pole widzenia ( fov_y
) i współczynnik kształtu ( vp[0]/vp[1]
). Macierz rzutowania musi odwzorować punkty z wnętrza piramidy na NDC zdefiniowane przez punkty (-1.0, -1.0, -1.0)
i (1.0, 1.0, 1.0)
. W tym momencie nasza piramida jest nieskończona, nie ma końca głębokości i nie możemy odwzorować nieskończonej przestrzeni na skończoną. W tym celu potrzebujemy teraz płaszczyzny bliskiej i dalekiej , przekształcają piramidę w prążek, przecinając górę i ograniczając piramidę w głębokości. Płaszczyzna bliska i daleka muszą być wybrane w taki sposób, aby obejmowały wszystko, co powinno być widoczne ze sceny.
Odwzorowanie punktów w obrębie frustum na NDC jest czystą matematyką i ogólnie można je rozwiązać. Rozwój formuł był często omawiany i wielokrotnie publikowany w Internecie. Ponieważ nie można wstawić formuły LaTeX do dokumentacji przepełnienia stosu, jest to zbędne w tym przypadku i dodawany jest tylko kompletny kod źródłowy C ++ i Python. Zauważ, że współrzędne oka są zdefiniowane w układzie współrzędnych po prawej stronie, ale NDC używa układu współrzędnych po lewej stronie. Macierz projekcji jest obliczana na podstawie pola widzenia fov_y
, współczynnika kształtu vp[0]/vp[1]
, bliskiej płaszczyzny near
i dalekiej płaszczyzny 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 } };
}
Pyton
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 ] ] )
Ustaw wygląd sceny - Wyświetl matrycę
W układzie współrzędnych w rzutni oś Y skierowana jest w górę (0, 1, 0)
a oś X skierowana w prawo (1, 0, 0)
. To powoduje, że oś Z wskazuje na rzutnię ( (0, 0, -1) = cross( X-axis, Y-axis )
).
W scenie oś X wskazuje na wschód, oś Y na północ, a oś Z na górę.
Oś X rzutni (1, 0, 0)
odpowiada osi Y sceny (1, 0, 0)
, oś Y rzutni (0, 1, 0 )
odpowiada osi Z sceny (0, 0, 1)
a oś Z rzutni (0, 0, 1 )
odpowiada zanegowanej osi Y sceny (0, -1, 0)
.
Dlatego każdy punkt i każdy wektor z układu odniesienia sceny należy najpierw przekonwertować na współrzędne rzutni. Można tego dokonać przez niektóre operacje zamiany i inwersji w wektorach skalarnych.
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
W konfiguracji macierzy widok z pozycji pos
, celem target
i wektor się up
mają być mapowane do współrzędnych obszaru wyświetlania, w sposób opisany powyżej. Daje to 2 punkty p
i t
oraz wektor u
, jak w poniższym fragmencie kodu. Oś Z macierzy widoku jest odwrotną linią wzroku obliczaną przez p - t
. Oś Y to wektor u
. Oś X jest obliczana na podstawie iloczynu poprzecznego osi Y i osi Z. Do ortonormalizacji macierzy widoku po raz drugi stosuje się iloczyn krzyżowy, aby obliczyć oś Y z osi Z i osi X (Oczywiście ortogonalizacja Gram-Schmidta równie dobrze by działała). Na koniec, wszystkie 3 osie muszą być znormalizowane i położenie oczu pos
musi być ustawiony jako th pochodzenia macierzy widoku.
Poniższy kod definiuje macierz, która dokładnie opisuje kroki niezbędne do obliczenia spojrzenia na scenę:
- Konwersja współrzędnych modelu na współrzędne rzutni.
- Obróć w kierunku kierunku widoku.
- Przejście do pozycji oka
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;
}
pyton
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] ] )
Matryce są ostatecznie zapisywane w mundurach i używane w module cieniującym wierzchołki do przekształcania pozycji modelu.
Moduł cieniujący wierzchołków
W module cieniującym wierzchołki wykonywana jest jedna transformacja po drugiej.
- Matryca modelu przenosi obiekt (siatkę) na swoje miejsce w scenie. (Jest to wymienione wyłącznie ze względu na kompletność i nie zostało tu udokumentowane, ponieważ nie ma to nic wspólnego z widokiem sceny)
- Macierz widoku określa kierunek, z którego oglądana jest scena. Transformacja za pomocą macierzy widoku obraca obiekty sceny, dzięki czemu są one oglądane z pożądanego kierunku widzenia w odniesieniu do układu współrzędnych rzutni.
- Matryca projekcyjna przekształca obiekty z widoku równoległego w widok perspektywiczny.
#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;
}
Moduł cieniujący fragmenty
Moduł cieniujący fragmenty wymieniono tutaj wyłącznie w celu uzupełnienia. Praca została wykonana wcześniej.
#version 400
in vec3 vertCol;
out vec4 fragColor;
void main()
{
fragColor = vec4( vertCol, 1.0 );
}
Po skompilowaniu i polubieniu modułu cieniującego macierze mogą się łączyć ze zmiennymi jednolitymi.
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() );
Pyton
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() )
Ponadto dodałem cały zrzut kodu przykładu w języku Python (dodanie przykładu w C ++ niestety przekroczyłoby limit 30000 znaków). W tym przykładzie kamera porusza się eliptycznie wokół czworościanu w punkcie centralnym elipsy. Kierunek patrzenia jest zawsze skierowany na tetraeder.
Pyton
Aby uruchomić skrypt w języku Python, należy zainstalować 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()