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 het target of de gezichtslijn los .
  • 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 vliegtuig far . 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.

voer hier de afbeeldingsbeschrijving in

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.

voer hier de afbeeldingsbeschrijving in

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:

  1. Modelcoördinaten omzetten in viewport-coördinaten.
  2. Roteer in de richting van de kijkrichting.
  3. 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.

  1. 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)
  2. 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.
  3. 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()


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow