サーチ…
前書き
モデル行列、ビュー行列、正投影および透視投影について
OGL 4.0 GLSL 400でカメラを実装する
シーンをカメラで撮影したかのように見たい場合は、最初にいくつか定義する必要があります。
- シーンを見る位置、目の位置
pos
。 - シーン(
target
)で見るポイント。私たちが見る方向を定義することも一般的です。技術的には視線が必要です。 1つの直線上の直線は、2点または点とベクトルのいずれかによって数学的に定義されます。定義の最初の部分は、目の位置と第2のいずれかであるtarget
または視線ベクトルlos
。 - 上方向
up
。 - 視野 '
fov_y
。これは、目の位置から始まり、一番左の点と一番右の点で終わり、同時に見ることができる2本の直線の間の角度を意味します。 - 大型と私たちは私たちの画像投影するビューポートのアスペクト比
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
シーンを描画するときにこのすべての情報を考慮するには、通常、投影行列とビュー行列が使用されます。シーンの個々の部分をシーンに配置するために、モデル行列が使用される。ただし、これらはここでは完全性のためにのみ記載されており、ここでは扱いません。
投影行列:投影行列は、ピンホールカメラから見た世界の3D点からビューポートの2D点へのマッピングを表します。
ビュー行列:ビュー行列は、 目の位置とシーンに視線方向を規定します。
モデル行列:モデル行列は、シーン内のオブジェクトの位置と相対的なサイズを定義します。
上記のデータ構造を対応するデータで埋めると、それらを適切な行列に変換する必要があります。 OGL互換モードでは、これはで行うことができますgluLookAt
とgluPerspective
制服に建てられた設定機能gl_ModelViewMatrix
、 gl_NormalMatrix
、およびgl_ModelViewProjectionMatrix
。 OGL 3.1とGLSL #version 150では、固定機能マトリックススタック全体が推奨されなくなったため、組み込みの制服は削除されました。 GLSLバージョン330以降のOGL高水準シェーダを使用する場合は、マトリクスユニフォームを独自に定義して設定する必要があります(GLSL compatibility
キーワードとは別に)。
パースペクティブを設定する - 投影行列
ビューポート上のポイントは、ポイント(-1.0, -1.0, -1.0)
と(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)
(-1.0, -1.0, -1.0)
定義されるNDCに点をマッピングする必要があります。この時点では、ピラミッドは無限大で終わりがなく、無限の空間を有限の空間に写像することはできません。このためには、 近平面と遠平面が必要です。頂点を切り取り、深さのピラミッドを限定することによって、ピラミッドを錐台に変換します。ニアプレーンとファープレーンは、シーンから見えるべきものがすべて含まれるように選択する必要があります。
錐台内の点からNDCへのマッピングは純粋な数学であり、一般的に解くことができます。数式の開発は、しばしばウェブを通じて議論され、繰り返し公開されました。 Stack OverflowのドキュメントにLaTeXの式を挿入することはできないので、これはここでは省略され、完成した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)
0,1,0 (0, 1, 0)
を示し、X軸は右向き(1, 0, 0)
ます。これにより、ビューポート( (0, 0, -1) = cross( X-axis, Y-axis )
)を指すZ軸が得られます。
シーンでは、X軸が東を、Y軸が北に、Z軸が上を指しています。
ビューポートのX軸(1, 0, 0)
シーンのY軸に一致(1, 0, 0)
ビューポートのY軸(0, 1, 0 )
シーンのZ軸と一致して(0, 0, 1)
ビューポート(0, 0, 1 )
(0, 0, 1)
のZ軸はシーン(0, -1, 0)
(0, 0, 1 )
のネゲートされたY軸に一致します。
したがって、シーンの参照システムからの各点および各ベクトルは、最初にビューポート座標に変換されなければなりません。これは、スカラーベクトル内のいくつかのスワッピングおよび反転演算によって行うことができる。
x y z
--------
1 0 0 | x' = x
0 0 1 | y' = z
0 -1 0 | z' = -y
ビュー行列を設定するには、上述のように、位置pos
、ターゲットtarget
およびアップベクトルup
をビューポート座標系にマッピングup
必要があります。次のコードスニペットのように、2点p
とt
とベクトルu
与えます。ビューマトリックスのZ軸はp - t
によって計算される逆視線です。 Y軸はアップベクトルu
です。 X軸は、Y軸とZ軸の外積によって計算されます。ビュー行列を正規直交化するために、クロス積を2回使用して、Z軸とX軸からY軸を計算します(もちろん、Gram-Schmidtの直交化も同様に機能します)。最後に、すべての3つの軸を正規化し、 視点位置 pos
をビュー行列の原点として設定する必要があります。
以下のコードは、シーンの外観を計算するために必要なステップを正確にカプセル化した行列を定義しています。
- モデル座標をビューポート座標に変換します。
- 視線方向に回転する。
- 目の位置への移動
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;
}
パイソン
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] ] )
行列は最終的に制服で書かれ、モデルの位置を変換するために頂点シェーダで使用されます。
頂点シェーダ
頂点シェーダでは、もう一方の変換が実行されます。
- モデル行列は、オブジェクト(メッシュ)をシーン内のその場所に持ち込みます。 (これは、完全性のためにリストされているだけで、シーンのビューとは関係がないため、ここでは文書化されていません)
- ビューマトリックスは、シーンを見る方向を定義します。ビュー・マトリックスによる変換は、シーンのオブジェクトを回転させ、ビューポートの座標系を参照して、それらを所望のビュー方向から見るようにする。
- 投影行列は、オブジェクトを平行ビューからパースビューに変換します。
#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()