Recherche…


Introduction

A propos de la matrice du modèle, de la matrice de vue, de la projection orthographique et en perspective

Implémenter une caméra dans OGL 4.0 GLSL 400

Si nous voulons regarder une scène comme si nous l'avions photographiée avec un appareil photo, nous devons d'abord définir certaines choses:

  • La position à partir de laquelle la scène est vue, la position des yeux pos .
  • Le point que nous regardons dans la scène ( target ). Il est également courant de définir la direction dans laquelle nous regardons. Techniquement, nous avons besoin d'une ligne de vue . Une droite dans l'espace est définie mathématiquement soit par 2 points, soit par un point et un vecteur. La première partie de la définition est la position de l'œil et la seconde est la target ou la ligne de visée vectorielle los .
  • La direction vers le up .
  • Le champ de vision ' fov_y . Cela signifie que l'angle entre les deux lignes droites commence à la position de l'œil et se termine au point le plus à gauche et au point le plus à droite, ce qui peut être vu simultanément.
  • Le grand et le format de la fenêtre dans laquelle nous projetons notre image vp .
  • L' avion near et le plan far . Le plan proche est la distance entre la position de l'œil et le plan d'où les objets deviennent visibles pour nous. Le plan lointain est la distance entre la position de l'œil et le plan sur lequel les objets de la scène sont visibles. Une explication de ce que l' avion proche et l' avion lointain doivent suivre suivront plus tard.

Une définition de ces données en C ++ et en Python peut ressembler à ceci:

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

Afin de prendre en compte toutes ces informations lors du dessin d'une scène, une matrice de projection et une matrice de vue sont généralement utilisées. Afin d'organiser les différentes parties d'une scène dans la scène, des matrices de modèle sont utilisées. Cependant, ceux-ci ne sont mentionnés ici que par souci d'exhaustivité et ne seront pas traités ici.

  • Matrice de projection: La matrice de projection décrit la correspondance entre les points 3D du monde tels qu'ils sont vus depuis une caméra à sténopé et les points 2D de la fenêtre.

  • Afficher la matrice: La matrice de vue définit la position de l' œil et la direction de visualisation sur la scène.

  • Matrice de modèle: La matrice de modèle définit l'emplacement et la taille relative d'un objet dans la scène.

Après avoir rempli les structures de données ci-dessus avec les données correspondantes, nous devons les traduire dans les matrices appropriées. Dans le mode de compatibilité OGL, cela peut être fait avec les fonctions gluLookAt et gluPerspective qui définissent les uniformes gl_ModelViewMatrix , gl_NormalMatrix et gl_ModelViewProjectionMatrix . Dans OGL 3.1 et GLSL #version 150, les uniformes intégrés ont été supprimés car toute la pile de matrice à fonction fixe est devenue obsolète. Si nous voulons utiliser le shader de haut niveau OGL avec GLSL version 330 ou plus, nous devons définir et définir la matrice uniformément notre propre (à part l'utilisation du mot clé de compatibility GLSL).

Mettre en place la perspective - matrice de projection

Un point de la fenêtre d'affichage est visible lorsqu'il se trouve dans le bloc AABB natif défini par les points (-1.0, -1.0, -1.0) et (1.0, 1.0, 1.0) . C'est ce qu'on appelle les coordonnées de périphérique normalisées (NDC). Un point avec les coordonnées (-1.0, -1.0, z) sera peint dans le coin inférieur gauche de la fenêtre et un point avec les coordonnées (1.0, 1.0, z) sera peint dans le coin supérieur droit de la fenêtre. La coordonnée Z est mappée de l'intervalle (-1.0, 1.0) à l'intervalle (0.0, 1.0) et écrite dans le Z-buffer.

Tout ce que nous pouvons voir de la scène se trouve dans une pyramide à 4 faces. Le sommet de la pyramide est la position de l' œil . Les 4 côtés de la pyramide sont définis par le champ de vision ( fov_y ) et le format ( vp[0]/vp[1] ). La matrice de projection doit mapper les points de l'intérieur de la pyramide au NDC défini par les points (-1.0, -1.0, -1.0) et (1.0, 1.0, 1.0) . À ce stade, notre pyramide est infinie, elle n’a pas de fin en profondeur et nous ne pouvons pas mapper un espace infini à un espace fini. Pour cela, nous avons maintenant besoin du plan proche et du plan lointain , ils transforment la pyramide en un tronc en coupant le sommet et en limitant la pyramide dans la profondeur. Le plan proche et le plan lointain doivent être choisis de manière à inclure tout ce qui doit être visible de la scène.

entrer la description de l'image ici

La correspondance entre les points d'un tronc et le NDC est purement mathématique et peut être généralement résolue. Le développement des formules a souvent été discuté et publié à plusieurs reprises sur le Web. Comme vous ne pouvez pas insérer une formule LaTeX dans une documentation Stack Overflow, cela n'est pas le cas ici et seul le code source C ++ et Python complété est ajouté. Notez que les coordonnées oculaires sont définies dans le système de coordonnées droitier, mais que NDC utilise le système de coordonnées gaucher. La matrice de projection est calculée à partir du champ de vision fov_y , du ratio d'aspect vp[0]/vp[1] , du plan near et du plan 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 ] ] )

Mettre en place le regard sur la scène - Afficher la matrice

Dans le système de coordonnées de la fenêtre, l'axe Y est orienté vers le haut (0, 1, 0) et l'axe X est orienté vers la droite (1, 0, 0) . Il en résulte un axe Z qui pointe hors de la fenêtre ( (0, 0, -1) = cross( X-axis, Y-axis ) ).

Dans la scène, l'axe X pointe vers l'est, l'axe Y vers le nord et l'axe Z vers le haut.

entrer la description de l'image ici

L'axe X de la fenêtre (1, 0, 0) correspond à l'axe Y de la scène (1, 0, 0) , l'axe Y de la fenêtre (0, 1, 0 ) correspond à l'axe Z de la scène (0, 0, 1) et l'axe Z de la fenêtre (0, 0, 1 ) correspond à l'axe Y négatif de la scène (0, -1, 0) .

Chaque point et chaque vecteur du système de référence de la scène doivent donc être convertis d’abord en coordonnées de fenêtre. Cela peut être fait par des opérations de permutation et d’inversion dans les vecteurs scalaires.

 x  y  z
--------
 1  0  0  | x' =  x
 0  0  1  | y' =  z
 0 -1  0  | z' = -y

Pour configurer une matrice de vues, la position pos , la cible target et le vecteur up doivent être mappés dans le système de coordonnées de la fenêtre, comme décrit ci-dessus. Cela donne les 2 points p et t et le vecteur u , comme dans l'extrait de code suivant. L'axe Z de la matrice de vue est la ligne de visée inverse, calculée par p - t . L'axe Y est le vecteur ascendant u . L'axe X est calculé par le produit croisé de l'axe Y et de l'axe Z. Pour orthonormaliser la matrice de vue, le produit croisé est utilisé une seconde fois pour calculer l'axe Y à partir de l'axe Z et de l'axe X (bien sûr, l'orthogonalisation de Gram-Schmidt fonctionnerait aussi bien). A la fin, tous les 3 essieux doivent être normalisées et la position des yeux pos doit être défini comme e origine de la matrice de vue.

Le code ci-dessous définit une matrice qui encapsule exactement les étapes nécessaires pour calculer un regard sur la scène:

  1. Conversion des coordonnées du modèle en coordonnées de fenêtre.
  2. Tournez dans le sens de la direction de vue.
  3. Mouvement vers la position de l'oeil

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] ] )

Les matrices sont finalement écrites en uniformes et utilisées dans le vertex shader pour transformer les positions du modèle.

Vertex Shader

Dans le vertex shader, une transformation après l'autre est effectuée.

  1. La matrice modèle amène l'objet (maillage) à sa place dans la scène. (Ceci n'est indiqué que par souci d'exhaustivité et n'a pas été documenté ici car il n'a rien à voir avec la vue de la scène)
  2. La matrice de vue définit la direction à partir de laquelle la scène est vue. La transformation avec la matrice de vues fait pivoter les objets de la scène de manière à ce qu'ils soient visualisés dans la direction de vue souhaitée par rapport au système de coordonnées de la fenêtre d'affichage.
  3. La matrice de projection transforme les objets d'une vue parallèle en une vue en perspective.
#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

Le fragment shader est répertorié ici uniquement pour des raisons d'exhaustivité. Le travail a été fait avant.

#version 400

in vec3 vertCol;

out vec4 fragColor;

void main()
{
    fragColor = vec4( vertCol, 1.0 );
}

Une fois le shader compilé et aimé, les matrices peuvent être liées aux variables uniformes.

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() )

De plus, j'ai ajouté l'intégralité du code dump d'un exemple Python (ajouter l'exemple C ++ dépasserait malheureusement la limite de 30000 caractères). Dans cet exemple, la caméra se déplace elliptiquement autour d'un tétraèdre à un point focal de l'ellipse. La direction de visionnement est toujours dirigée vers le tetraeder.

Python

Pour exécuter le script Python, NumPy doit être installé.

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
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow