Suche…


Einführung

Über Modellmatrix, Ansichtsmatrix, orthographische und perspektivische Projektion

Implementieren Sie eine Kamera in OGL 4.0 GLSL 400

Wenn wir eine Szene so betrachten möchten, als hätten wir sie mit einer Kamera fotografiert, müssen wir zunächst einige Dinge definieren:

  • Die Position, von der aus die Szene betrachtet wird, die Augenposition pos .
  • Der Punkt, den wir in der Szene betrachten ( target ). Es ist auch üblich, die Richtung festzulegen, in die wir schauen. Technisch brauchen wir eine Sichtverbindung . Eine Gerade im Raum wird mathematisch entweder durch 2 Punkte oder durch einen Punkt und einen Vektor definiert. Der erste Teil der Definition ist die Augenposition und die zweite ist entweder das target oder die Sichtlinie Vektor los .
  • Die Richtung nach up .
  • Das Sichtfeld ' fov_y . Dies ist der Winkel zwischen den beiden geraden Linien, beginnend an der Augenposition und endet am äußersten linken Punkt und am äußersten rechten Punkt, die gleichzeitig sichtbar sind.
  • Das große und das Seitenverhältnis des Ansichtsfensters, auf das wir unser Bild vp .
  • Die nahe Ebene near und die entfernte Ebene far . Die nahe Ebene ist der Abstand von der Augenposition zu der Ebene, von der aus die Objekte für uns sichtbar werden. Die ferne Ebene ist der Abstand von der Augenposition zu der Ebene, in der die Objekte der Szene für uns sichtbar sind. Eine Erklärung dazu, was für die Nahebene und die Fernebene benötigt wird, folgt später.

Eine Definition dieser Daten in C ++ und Python kann folgendermaßen aussehen:

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

Um all diese Informationen beim Zeichnen einer Szene zu berücksichtigen, werden normalerweise eine Projektionsmatrix und eine Ansichtsmatrix verwendet. Um die einzelnen Teile einer Szene in der Szene anzuordnen, werden Modellmatrizen verwendet. Diese werden hier jedoch nur der Vollständigkeit halber erwähnt und werden hier nicht behandelt.

  • Projektionsmatrix: Die Projektionsmatrix beschreibt die Abbildung von 3D-Punkten in der Welt, wie sie von einer Lochkamera aus gesehen werden, zu 2D-Punkten des Ansichtsfensters.

  • Ansichtsmatrix: Die Ansichtsmatrix definiert die Augenposition und die Blickrichtung auf die Szene.

  • Modellmatrix: Die Modellmatrix definiert die Position und die relative Größe eines Objekts in der Szene.

Nachdem wir die obigen Datenstrukturen mit den entsprechenden Daten gefüllt haben, müssen wir sie in die entsprechenden Matrizen übersetzen. Im OGL-Kompatibilitätsmodus können dies mit den Funktionen gluLookAt und gluPerspective werden, die die integrierten Uniformen gl_ModelViewMatrix , gl_NormalMatrix und gl_ModelViewProjectionMatrix . In OGL 3.1 und GLSL #version 150 wurden die eingebauten Uniformen entfernt, da der gesamte Matrixstack mit fester Funktion veraltet war. Wenn Sie OGL-High-Level-Shader mit GLSL-Version 330 oder höher verwenden möchten, müssen Sie die Matrixuniformen selbst definieren und festlegen (abgesehen von der Verwendung des GLSL- compatibility ).

Richten Sie die Perspektive ein - Projektionsmatrix

Ein Punkt im Darstellungsbereich ist sichtbar, wenn er sich in der durch die Punkte (-1.0, -1.0, -1.0) und (1.0, 1.0, 1.0) definierten nativen AABB (Achsausrichtungs-Begrenzungsbox (1.0, 1.0, 1.0) . Dies wird als normalisierte Gerätekoordinate (NDC) bezeichnet. Ein Punkt mit den Koordinaten (-1.0, -1.0, z) wird in der unteren linken Ecke des Ansichtsfensters und ein Punkt mit den Koordinaten (1.0, 1.0, z) in der oberen rechten Ecke des Ansichtsfensters dargestellt. Die Z-Koordinate wird vom Intervall (-1,0, 1,0) bis zum Intervall (0,0, 1,0) abgebildet und in den Z-Puffer geschrieben.

Alles, was wir von der Szene aus sehen können, befindet sich in einer vierseitigen Pyramide. Die Spitze der Pyramide ist die Augenposition . Die vier Seiten der Pyramide werden durch das Sichtfeld ( fov_y ) und das Aspektverhältnis ( vp[0]/vp[1] ) definiert. Die Projektionsmatrix muss die Punkte aus dem Inneren der Pyramide auf den NDC abbilden, der durch die Punkte (-1.0, -1.0, -1.0) und (1.0, 1.0, 1.0) . An diesem Punkt ist unsere Pyramide unendlich, sie hat kein Ende in der Tiefe und wir können einen unendlichen Raum nicht einem endlichen Raum zuordnen. Dafür brauchen wir jetzt die nahe Ebene und die ferne Ebene , sie verwandeln die Pyramide in ein Kegelstumpf, indem sie die Spitze durchschneiden und die Pyramide in der Tiefe begrenzen. Die Nahebene und die Fernebene müssen so gewählt werden, dass sie alles enthalten, was von der Szene aus sichtbar sein soll.

Geben Sie hier die Bildbeschreibung ein

Die Abbildung von den Punkten innerhalb eines Kegelstumpfes auf das NDC ist reine Mathematik und kann im Allgemeinen gelöst werden. Die Entwicklung der Formeln wurde häufig im Web diskutiert und wiederholt veröffentlicht. Da Sie eine LaTeX-Formel nicht in eine Stack Overflow-Dokumentation einfügen können, wird hier verzichtet und nur der vollständige C ++ - und Python-Quellcode hinzugefügt. Beachten Sie, dass die Augenkoordinaten im rechtshändigen Koordinatensystem definiert sind, NDC jedoch das linkshändige Koordinatensystem verwendet. Die Projektionsmatrix wird aus dem Sichtfeld fov_y , dem Aspektverhältnis vp[0]/vp[1] , der nahen Ebene near und der fernen Ebene far vp[0]/vp[1] .

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

Richten Sie den Blick auf die Szene ein - Matrix anzeigen

Im Koordinatensystem im Ansichtsfenster zeigt die Y-Achse nach oben (0, 1, 0) und die X-Achse nach rechts (1, 0, 0) . Dies führt zu einer Z-Achse, die aus dem Darstellungsbereich ( (0, 0, -1) = cross( X-axis, Y-axis ) ) zeigt.

In der Szene zeigt die X-Achse nach Osten, die Y-Achse nach Norden und die Z-Achse nach oben.

Geben Sie hier die Bildbeschreibung ein

Die X-Achse des Ansichtsfensters (1, 0, 0) entspricht der Y-Achse der Szene (1, 0, 0) , die Y-Achse des Ansichtsfensters (0, 1, 0 ) entspricht der Z-Achse der Szene (0, 0, 1) und die Z-Achse des Ansichtsfensters (0, 0, 1 ) entspricht der negierten Y-Achse der Szene (0, -1, 0) .

Jeder Punkt und jeder Vektor aus dem Bezugssystem der Szene muss daher zuerst in Ansichtsfensterkoordinaten umgewandelt werden. Dies kann durch einige Swapping- und Invertierungsoperationen in den Skalarvektoren erfolgen.

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

Zur Einrichtung einer Ansicht Matrix der Position pos , das Ziels target und die Aufwärts - Vektor up werden müssen , in die Darstellungsfeld-Koordinatensystem abgebildet, wie oben beschrieben. Dies ergibt die 2 Punkte p und t und den Vektor u , wie im folgenden Codeausschnitt. Die Z-Achse der Ansichtsmatrix ist die umgekehrte Sichtlinie, die mit p - t berechnet wird. Die Y-Achse ist der Aufwärtsvektor u . Die X-Achse wird aus dem Kreuzprodukt von Y-Achse und Z-Achse berechnet. Zur Orthonormalisierung der Ansichtsmatrix wird das Kreuzprodukt ein zweites Mal verwendet, um die Y-Achse aus der Z-Achse und der X-Achse zu berechnen (natürlich funktioniert auch die Gram-Schmidt-Orthogonalisierung). Am Ende müssen alle 3 Achsen normalisiert und die Augenposition pos als Ursprung der Ansichtsmatrix gesetzt werden.

Der folgende Code definiert eine Matrix, die genau die Schritte beinhaltet, die erforderlich sind, um einen Blick auf die Szene zu berechnen:

  1. Modellkoordinaten in Viewport-Koordinaten konvertieren.
  2. In Richtung der Blickrichtung drehen.
  3. Bewegung in die Augenposition

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

Die Matrizen werden schließlich in Uniformen geschrieben und im Vertex-Shader zur Transformation der Modellpositionen verwendet.

Vertex-Shader

Im Vertex-Shader wird eine Transformation nach der anderen ausgeführt.

  1. Die Modellmatrix bringt das Objekt (Netz) an seinen Platz in der Szene. (Dies ist nur der Vollständigkeit halber aufgeführt und wurde hier nicht dokumentiert, da es nichts mit dem Blick auf die Szene zu tun hat.)
  2. Die Ansichtsmatrix definiert die Richtung, aus der die Szene betrachtet wird. Die Transformation mit der Ansichtsmatrix dreht die Objekte der Szene so, dass sie aus der gewünschten Blickrichtung in Bezug auf das Koordinatensystem des Ansichtsfensters betrachtet werden.
  3. Die Projektionsmatrix transformiert die Objekte aus einer parallelen Ansicht in eine perspektivische Ansicht.
#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

Der Fragment-Shader ist hier nur der Vollständigkeit halber aufgeführt. Die Arbeit wurde vorher erledigt.

#version 400

in vec3 vertCol;

out vec4 fragColor;

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

Nachdem der Shader kompiliert und beliebt ist, können die Matrizen an die einheitlichen Variablen gebunden werden.

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

Außerdem habe ich den gesamten Code-Dump eines Python-Beispiels hinzugefügt (das Hinzufügen des C ++ - Beispiels würde leider die Grenze von 30000 Zeichen überschreiten). In diesem Beispiel bewegt sich die Kamera in einem Brennpunkt der Ellipse elliptisch um ein Tetraeder. Die Blickrichtung ist immer auf den Tetraeder gerichtet.

Python

Um das Python-Skript auszuführen, muss NumPy installiert sein.

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
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow