在 OGL 4.0 GLSL 400 中實現相機

如果我們想要看一個場景就像我們用相機拍攝它一樣,我們必須首先定義一些東西:

  • 觀看場景的位置,眼睛位置 pos
  • 我們在場景中看到的這一點(target)。定義我們看起來的方向也很常見。從技術上講,我們需要一個視線。在空間中的一個直線在數學上由 2 個點或由點和向量定義。定義的第一部分是眼睛位置,第二部分是 target 或視線向量 los
  • 方向向上 up
  • 視野’fov_y。這意味著兩條直線之間的角度,從眼睛位置開始,到最左邊的點和最右邊的點,可以同時看到。
  • 我們投影影象的視口的大小和寬高比 vp
  • 近平面 near遠平面 far。在近平面是從眼睛的位置,以飛機從那裡的物件成為我們可視距離。該遠平面是從眼睛的位置到場景的物件是我們看到的平面之間的距離。稍後將詳細說明需要近平面遠平面的內容。

C++和 Python 中對此資料的定義可能如下所示:

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

為了在繪製場景時考慮所有這些資訊,通常使用投影矩陣和檢視矩陣。為了在場景中佈置場景的各個部分,使用模型矩陣。但是,這裡只是為了完整起見而提到這些,這裡不再討論。

  • 投影矩陣:投影矩陣描述了從針孔攝像機到視口的 2D 點看世界中 3D 點的對映。

  • 檢視矩陣:檢視矩陣定義場景中的眼睛位置和觀察方向。

  • 模型矩陣:模型矩陣定義場景中物件的位置和相對大小。

在我們用相應的資料填充上面的資料結構之後,我們必須將它們轉換成適當的矩陣。在 OGL 相容模式下,這可以通過設定內建制服 gl_ModelViewMatrixgl_NormalMatrixgl_ModelViewProjectionMatrixgluLookAtgluPerspective 功能來完成。在 OGL 3.1 和 GLSL #version 150 中,內建的制服被移除,因為整個固定功能矩陣堆疊已被棄用。如果我們想要使用 GLSL 版本 330 甚至更高版本的 OGL 高階著色器,我們必須定義和設定我們自己的矩陣制服(除了使用 GLSL compatibility 關鍵字)。

設定透視 - 投影矩陣

當視口中的點位於由點 (-1.0, -1.0, -1.0)(1.0, 1.0, 1.0) 定義的本機 AABB(軸對齊的邊界框)中時,它是可見的。這稱為標準化裝置座標(NDC)。座標為 (-1.0, -1.0, z) 的點將繪製到視口的左下角,座標為 (1.0, 1.0, z) 的點將繪製到視口的右上角。Z 座標從間隔(-1.0,1.0)對映到間隔(0.0,1.0)並寫入 Z 緩衝區。

我們從現場看到的只是一個 4 面金字塔。金字塔的頂部是眼睛的位置。金字塔的四邊由視野(fov_y)和縱橫比(vp[0]/vp[1])定義。投影矩陣必須將金字塔內部的點對映到由點 (-1.0, -1.0, -1.0)(1.0, 1.0, 1.0) 定義的 NDC。在這一點上,我們的金字塔是無限的,它沒有深度,我們無法將無限空間對映到有限空間。為此,我們現在需要近平面遠平面,它們通過切割頂部並限制深度中的金字塔將金字塔轉換為平截頭體。必須以這樣的方式選擇近平面和遠平面,使得它們包括從場景中可見的所有東西。

StackOverflow 文件

從平截頭體內的點到 NDC 的對映是純數學,並且通常可以解決。公式的發展經常在整個網路上進行討論和重複發表。由於你無法將 LaTeX 公式插入到 Stack Overflow 文件中,因此此處不再使用,只新增了完整的 C++和 Python 原始碼。請注意,眼睛座標是在右手座標系中定義的,但 NDC 使用左手座標系。投影矩陣由視 fov_y縱橫比 vp[0]/vp[1]近平面 near遠平面 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 ] ] )

設定場景外觀 - 檢視矩陣

在視口的座標系中,Y 軸向上指向 (0, 1, 0),X 軸指向右側 (1, 0, 0)。這導致 Z 軸指向視口((0, 0, -1) = cross( X-axis, Y-axis ))。

在場景中,X 軸指向東,Y 軸指向北,Z 軸指向頂部。

StackOverflow 文件

視口 (1, 0, 0) 的 X 軸與場景 (1, 0, 0) 的 Y 軸匹配,視口 (0, 1, 0 ) 的 Y 軸與場景 (0, 0, 1) 的 Z 軸匹配,視口 (0, 0, 1 ) 的 Z 軸與場景 (0, -1, 0) 的否定 Y 軸匹配。

因此,必須首先將來自場景參考系統的每個點和每個向量轉換為視口座標。這可以通過標量向量中的一些交換和反轉操作來完成。

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

要設定檢視矩陣,必須將位置 pos,目標 target 和向上向量 up 對映到視口座標系,如上所述。這給出了 2 分 pt 以及向量 u,如下面的程式碼片段所示。檢視矩陣的 Z 軸是反向視線,由 p - t 計算。Y 軸是向上向量 u。X 軸由 Y 軸和 Z 軸的叉積計算。對於檢視矩陣的正交化,第二次使用叉積來計算 Z 軸和 X 軸的 Y 軸(當然,Gram-Schmidt 正交化也可以正常工作)。最後,必須對所有 3 個軸進行歸一化,並且必須將眼睛位置 pos 設定為檢視矩陣的原點。

下面的程式碼定義了一個矩陣,它完全封裝了計算場景外觀所需的步驟:

  1. 將模型座標轉換為視口座標。
  2. 沿視線方向旋轉。
  3. 運動到眼睛的位置

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

矩陣最終以制服編寫,並在頂點著色器中用於轉換模型位置。

頂點著色器

在頂點著色器中,執行另一個轉換。

  1. 模型矩陣將物件(網格)帶到場景中的位置。 (這只是為了完整性而列出,因為它與場景的檢視無關,所以這裡沒有記錄)
  2. 檢視矩陣定義了檢視場景的方向。使用檢視矩陣的變換旋轉場景的物件,以便參考視口的座標系從所需的檢視方向檢視它們。
  3. 投影矩陣將物件從平行檢視轉換為透檢視。
#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;
}

片段著色器

此處列出的片段著色器僅用於完整性。這項工作以前完成了。

#version 400

in vec3 vertCol;

out vec4 fragColor;

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

在編譯和喜歡著色器之後,矩陣可以繫結到統一變數。

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

另外,我已經新增了 Python 示例的整個程式碼轉儲(不幸的是,新增 C++示例會超過 30000 個字元的限制)。在該示例中,相機在橢圓的焦點處圍繞四面體橢圓地移動。觀察方向始終指向四邊形。

Python

要執行 Python 指令碼,必須安裝 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()