Ricerca…


introduzione

Informazioni sulla matrice del modello, sulla matrice di visualizzazione, sulla proiezione ortogonale e prospettica

Implementare una fotocamera in OGL 4.0 GLSL 400

Se vogliamo guardare una scena come se l'avessimo fotografata con una fotocamera, dobbiamo prima definire alcune cose:

  • La posizione da cui viene vista la scena, la posizione dell'occhio pos .
  • Il punto che guardiamo nella scena ( target ). È anche comune definire la direzione in cui guardiamo. Tecnicamente abbiamo bisogno di una linea di vista . Una retta nello spazio è definita matematicamente da 2 punti o da un punto e un vettore. La prima parte della definizione è la posizione dell'occhio e il 2o è il target o il vettore della linea di vista los .
  • La direzione verso l' up .
  • Il campo visivo ' fov_y . Ciò significa l'angolo tra le due linee rette, che inizia dalla posizione dell'occhio e termina nel punto più a sinistra e nel punto più a destra, che può essere visto contemporaneamente.
  • Il grande e il rapporto di aspetto del viewport a cui proiettiamo la nostra immagine vp .
  • L' aereo near e l' aereo far . Il piano vicino è la distanza dalla posizione dell'occhio al piano da cui gli oggetti diventano visibili a noi. Il piano lontano è la distanza dalla posizione dell'occhio al piano a cui gli oggetti della scena sono visibili. Una spiegazione di ciò che il piano vicino e il piano lontano sono necessari seguirà più tardi ..

Una definizione di questi dati in C ++ e in Python può apparire come questa:

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};
};

Pitone

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

Per tenere tutte queste informazioni in considerazione quando si disegna una scena, vengono solitamente utilizzate una matrice di proiezione e una matrice di viste. Per organizzare le singole parti di una scena nella scena, vengono utilizzate le matrici dei modelli. Tuttavia, questi sono menzionati qui solo per motivi di completezza e non saranno trattati qui.

  • Matrice di proiezione: la matrice di proiezione descrive la mappatura da punti 3D nel mondo così come sono visti da una camera stenopeica, a punti 2D della vista.

  • Visualizza matrice: la matrice di visualizzazione definisce la posizione dell'occhio e la direzione di visione sulla scena.

  • Matrice modello: la matrice modello definisce la posizione e la dimensione relativa di un oggetto nella scena.

Dopo aver riempito le strutture di dati sopra con i dati corrispondenti, dobbiamo tradurli nelle matrici appropriate. Nella modalità di compatibilità OGL, questo può essere fatto con le funzioni gluLookAt e gluPerspective che impostano le uniformi gl_ModelViewMatrix , gl_NormalMatrix e gl_ModelViewProjectionMatrix . In OGL 3.1 e GLSL #version 150 le uniformi incorporate sono state rimosse, perché l'intero stack matrix a funzione fissa è diventato deprecato. Se vogliamo utilizzare OGL ad alto livello con GLSL versione 330 o anche superiore, dobbiamo definire e impostare noi stessi le uniformi di matrice (a parte l'uso della parola chiave di compatibility GLSL).

Imposta la prospettiva - Matrice di proiezione

Un punto sul viewport è visibile quando si trova nell'AABB nativo (bounding box allineato asse) definito dai punti (-1.0, -1.0, -1.0) e (1.0, 1.0, 1.0) . Questo è chiamato Normalised Device Coordinates (NDC). Un punto con le coordinate (-1.0, -1.0, z) verrà dipinto nell'angolo in basso a sinistra della vista e un punto con le coordinate (1.0, 1.0, z) verrà dipinto nell'angolo superiore destro della finestra. La coordinata Z viene mappata dall'intervallo (-1.0, 1.0) all'intervallo (0.0, 1.0) e scritta nel buffer Z.

Tutto ciò che possiamo vedere dalla scena è all'interno di una piramide a 4 lati. La parte superiore della piramide è la posizione dell'occhio . I 4 lati della piramide sono definiti dal campo visivo ( fov_y ) e dal rapporto aspetto ( vp[0]/vp[1] ). La matrice di proiezione deve mappare i punti dall'interno della piramide all'NDC definito dai punti (-1.0, -1.0, -1.0) e (1.0, 1.0, 1.0) . A questo punto la nostra piramide è infinita, non ha fine in profondità e non possiamo mappare uno spazio infinito in uno finito. Per questo ora abbiamo bisogno del piano vicino e del piano lontano , trasformano la piramide in un tronco tagliando la cima e limitando la piramide in profondità. Il piano vicino e il piano lontano devono essere scelti in modo tale da includere tutto ciò che dovrebbe essere visibile dalla scena.

inserisci la descrizione dell'immagine qui

La mappatura dai punti all'interno di un tronco all'NDC è pura matematica e può essere generalmente risolta. Lo sviluppo delle formule è stato spesso discusso e pubblicato più volte su tutto il web. Dal momento che non è possibile inserire una formula LaTeX in una documentazione di Stack Overflow, qui non è disponibile e solo il codice sorgente C ++ e Python completato viene aggiunto. Notare che le coordinate dell'occhio sono definite nel sistema di coordinate della mano destra, ma NDC usa il sistema di coordinate della mano sinistra. La matrice di proiezione viene calcolata da, il campo di vista fov_y , il rapporto di aspetto vp[0]/vp[1] , il piano near e il lontano piano 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 } };
}

Pitone

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

Imposta l'aspetto della scena: Visualizza matrice

Nel sistema di coordinate sulla vista, l'asse Y punta verso l'alto (0, 1, 0) e l'asse X punta a destra (1, 0, 0) . Ne risulta un asse Z che indica fuori dalla vista ( (0, 0, -1) = cross( X-axis, Y-axis ) ).

Nella scena, l'asse X punta verso est, l'asse Y verso nord e l'asse Z verso l'alto.

inserisci la descrizione dell'immagine qui

L'asse X della vista (1, 0, 0) corrisponde all'asse Y della scena (1, 0, 0) , l'asse Y della vista (0, 1, 0 ) corrisponde all'asse Z della scena (0, 0, 1) e l'asse Z della vista (0, 0, 1 ) corrisponde all'asse Y negato della scena (0, -1, 0) .

Pertanto, ogni punto e ogni vettore dal sistema di riferimento della scena devono essere convertiti prima in coordinate di vista. Questo può essere fatto scambiando e invertendo le operazioni nei vettori scalari.

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

Per configurare una matrice di visualizzazione, la posizione pos , il target di target e il vettore up devono essere mappati nel sistema di coordinate del viewport, come descritto sopra. Ciò fornisce i 2 punti p e t e il vettore u , come nel seguente frammento di codice. L'asse Z della matrice della vista è la linea di vista inversa, che è calcolata da p - t . L'asse Y è il vettore up u . L'asse X viene calcolato dal prodotto incrociato dell'asse Y e dell'asse Z. Per ortonormalizzare la matrice della vista, il prodotto incrociato viene utilizzato una seconda volta, per calcolare l'asse Y dall'asse Z e l'asse X (naturalmente l'ortogonalizzazione Gram-Schmidt funzionerebbe altrettanto bene). Alla fine, tutti i 3 assi devono essere normalizzati e la posizione dell'occhio pos deve essere impostato come th origine della matrice vista.

Il codice sottostante definisce una matrice che racchiude esattamente i passaggi necessari per calcolare un aspetto della scena:

  1. Conversione delle coordinate del modello in coordinate del viewport.
  2. Ruota nella direzione della direzione della vista.
  3. Movimento alla posizione dell'occhio

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;
}

pitone

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

Le matrici vengono infine scritte in uniformi e utilizzate nel vertex shader per trasformare le posizioni del modello.

Vertex shader

Nel vertex shader, viene eseguita una trasformazione dopo l'altra.

  1. La matrice del modello porta l'oggetto (mesh) nella sua posizione nella scena. (Questo è elencato solo per motivi di completezza e non è stato documentato qui poiché non ha nulla a che fare con la vista della scena)
  2. La matrice della vista definisce la direzione da cui viene vista la scena. La trasformazione con la matrice della vista ruota gli oggetti della scena in modo che vengano visualizzati dalla direzione di visualizzazione desiderata con riferimento al sistema di coordinate della finestra.
  3. La matrice di proiezione trasforma gli oggetti da una vista parallela in una vista prospettica.
#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

Lo shader di frammenti è elencato qui solo per motivi di completezza. Il lavoro è stato fatto prima.

#version 400

in vec3 vertCol;

out vec4 fragColor;

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

Dopo che lo shader è stato compilato e apprezzato, le matrici possono essere associate alle variabili uniformi.

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

Pitone

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

Inoltre, ho aggiunto l'intero dump del codice di un esempio Python (per aggiungere l'esempio C ++, sfortunatamente, supererei il limite di 30000 caratteri). In questo esempio, la telecamera si muove in modo ellittico attorno a un tetraedro in corrispondenza di un punto focale dell'ellisse. La direzione di visione è sempre diretta al tetraeder.

Pitone

Per eseguire lo script Python deve essere installato 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()


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow