OGL(教程12)——透視投影
原文地址:http://ogldev.atspace.co.uk/www/tutorial12/tutorial12.html
背景知識:
我們最終來到了最重要的一節,把3D世界對映到2D平面,這個對映還要保留深度資訊。一個很好的例子是,鐵路軌道的圖片,在很遠的地方兩個軌道講匯聚於一點。
如圖:
我們準備推導一個變換以能夠給滿足上面的需求,我們還有一個需求,使用這個變換的時候,同時也把裁剪工作做了。我們把座標對映到-1到1之間。也就是說裁剪器不需要知道螢幕的維度,也不需要知道近平面和遠平面就能做裁剪工作。
透視變換需要我們提供四個引數:
- 寬高比——最終投射的矩形區域的寬度和高度的比例
- 垂直視野——攝像機的張角
- 近平面的座標——距離攝像機距離小於這個近平面的則裁剪掉
- 遠平面的座標——距離攝像機大於這個遠平面的物體會被裁剪掉
寬高比是需要的,由於我們需要使用規格化空間的座標,這個寬度和高度相同。由於很少遇到螢幕的寬度比高要大,也就是說頂點在寬度上要進行很大的壓縮。這使我們能夠在寬度上看到更多的畫素。
垂直視野允許我們能夠放大或者縮小世界。看下面的例子,在左圖中,張角比較大,這使物體看起來比較小,而右圖中張角比較下,這會使物體看起來比較大。但是這個和攝像機的位置有關係。
我們定義投射面和攝像機的距離。投射面是和xy螢幕平行的面。很顯然,不是整個螢幕都能夠看到的,因為它太大了。我們只能看到一個矩形區域,叫做透視視窗。這個視窗的比例和我們的螢幕的比例是一樣的。寬高比計算方式如下:
ar = 螢幕寬度 / 螢幕高度
為了方便,我們把透視視窗的高度定位2。這就是意味著寬度是比例的2倍。如果我們把攝像機放在原點,然後從攝像機的後面看的話,會是這樣:
任何在四邊形之外的都會被剔除掉。
現在我們從一邊看,如從yz平面看:
我們可以發現從攝像機到投射面的距離,可以使用垂直視角推匯出來:
下一步就是計算x和y投射座標。看下圖,依然是從yz平面看:
我們3D世界中有一個點座標為(x,y,z),我們需要在投射平面上,找到一個(xp,yp)點。由於x分量超出了區域,上圖x只想畫面的裡面或者外面。我們從y開始,利用相似三角形來推導:
對於x也是同樣的規則:
由於我們的投射視窗為2ar 寬度,高為2,如果我們計算出來的點的座標在-ar和+ar之間,而y在-1到1之間,那麼這個點就在投射平面內。所以y的座標在-1到1之間,但是x不是。我們也可以把x標準化,通過除以寬高比得到。這就意味著如果有一個點的投射為+ar,那麼則對應為+1,而且是右手邊的規格化的盒子裡。如果是對映的x是+0.5,寬高比為1.333(1024768),那麼新的x就為0.5/1.333=0.375。總的來說,寬高比影響了x的壓縮程度。
我們有如下的計算公式:
在完成整個過程之前,我們嘗試看看投射矩陣看起來像什麼。這就意味著使用矩陣來描述上面的公式。上面的兩個公式,我都除以z。但是z對於不同的頂點是不同的。所有你不能把它放在矩陣中進行處理。為了更好的理解,我們把最上面的一行向量寫為(a,b,c,d)。我們需要找到對應值使其滿足:
這是點乘,使用矩陣最上面的向量和頂點的座標點乘得到。我們把b和d置為0,我們不能找到a和c,在等式的左邊,然後使其等式成立。OpenGL的解決方式是把變換分為兩個部分:首先利用乘法,然後再除以z。這個矩陣由應用提供,shader必須中不需要使用它乘以位置左邊。除以z是又GPU執行,是在光柵化器中執行,這個執行的過程在頂點著色器和片段著色器之間進行。GPU是怎麼知道哪個頂點著色器的輸出需要除以z的呢?簡單的說內建的變數gl_Position被用來指定做這個事情。現在我們需要找到一個矩陣能夠代表上面的關於x和y的投射等式成立。
在乘以那個矩陣之後,GPU可以自動為我們除以z,我們可以得到想要的結果。但是這裡有另外一個複雜度的問題:如果我們的頂點乘以那個矩陣,然後再除以z,我們這裡暫時把所有定的z置為1。原始的z值必須保留起來,為了後面的深度測試使用。其中一個技巧是把初始的z保留到w分量中,然後讓xyz分量除以w,而不是除以z就可以了。w儲存了原始的z值,用於後面的深度測試使用。自動將gl_Position除以w的過程稱之為透視除法。
我們現在可以給出一箇中間的矩陣,它代表了上面的兩個等式,而且把z拷貝到了z分量。
如我之前說的,我們想把z單位化,讓其在裁剪的時候能夠方便些,而且不需要知道近平面和遠平面。但是,上面的矩陣把z置為了0。我們知道在變形之後,系統會自動執行透視除法,我們需要在矩陣的第三行設定一些值,使得透視除法之後,z值在近平面和遠平面之間(nearZ<=Z<=farZ),被對映到[-1,1]區間內。這個對映的過程包含兩個部分。首先縮放區間[NearZ,FarZ]到寬度為2的區間。然後平移這個區間使其從-1開始。縮放z值,然後做平移處理。
對上面的等式右邊做透視除法:
我們需要找到A和B值,使其對映到[-1,1]之間。我們知道z的最小值為-1的時候,對應的是近平面NearZ,而1對應的是遠平面FarZ,因此我們可以得出:
然後我們選擇矩陣的第三行(abcd),使其滿足:
我們可以立即將a和b設定為0,因為它們不對z的變換有影響。然後c就是我們上面的A,d就是上面的B,因為我麼知道w=1。。
因此,變換矩陣如下:
當頂點座標和這個透視矩陣相乘之後得到的座標被稱之為裁剪空間,然後再經過透視除法之後得到的座標就在NDC空間了。
從之前的一系列教程到現在變得越來越清晰了。如果沒有經過透視,我們只能簡單的從vs輸出xyz,且其範圍必須在[-1,+1]之間。這個就保證了他們能夠出現螢幕上。通過把w置為1,我們可以避免透視除法帶來的影響。在經過上面的變換之後,座標會被轉化到螢幕空間功。我們使用透視變換矩陣還有透視變換之後,就實現了3D到2D的對映。
程式碼註釋:
void Pipeline::InitPerspectiveProj(Matrix4f& m) const>
{
const float ar = m_persProj.Width / m_persProj.Height;
const float zNear = m_persProj.zNear;
const float zFar = m_persProj.zFar;
const float zRange = zNear - zFar;
const float tanHalfFOV = tanf(ToRadian(m_persProj.FOV / 2.0));
m.m[0][0] = 1.0f / (tanHalfFOV * ar);
m.m[0][1] = 0.0f;
m.m[0][2] = 0.0f;
m.m[0][3] = 0.0f;
m.m[1][0] = 0.0f;
m.m[1][1] = 1.0f / tanHalfFOV;
m.m[1][2] = 0.0f;
m.m[1][3] = 0.0f;
m.m[2][0] = 0.0f;
m.m[2][1] = 0.0f;
m.m[2][2] = (-zNear - zFar) / zRange;
m.m[2][3] = 2.0f * zFar * zNear / zRange;
m.m[3][0] = 0.0f;
m.m[3][1] = 0.0f;
m.m[3][2] = 1.0f;
m.m[3][3] = 0.0f;
}
一個叫做m_persProj的變數被新增到了Pipeline類中了。此變數包含了透視變換的配置。上面的程式碼就是我們的介紹的透視矩陣。
m_transformation = PersProjTrans * TranslationTrans * RotateTrans * ScaleTrans;
我們把透視矩陣放在第一個位置,然後匯出所有的矩陣。由於頂點位置是放在最右邊的,所以這個透視變換是最後做的。從右往左依次是,縮放、旋轉、平移、透視。
p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 1000.0f);
在渲染函式中,我們設定了透視的引數,執行程式觀察效果。
程式碼:
math_3d.h
#ifndef MATH_3D_H
#define MATH_3D_H
#include <math.h>
const float M_PI = 3.14f;
#define ToRadian(x) ((x) * M_PI / 180.0f)
#define ToDegree(x) ((x) * 180.0f / M_PI)
struct Vector3f
{
float x;
float y;
float z;
Vector3f()
{
}
Vector3f(float _x, float _y, float _z)
{
x = _x;
y = _y;
z = _z;
}
};
class Matrix4f
{
public:
float m[4][4];
Matrix4f()
{
}
inline void InitIdentity()
{
m[0][0] = 1.0f; m[0][1] = 0.0f; m[0][2] = 0.0f; m[0][3] = 0.0f;
m[1][0] = 0.0f; m[1][1] = 1.0f; m[1][2] = 0.0f; m[1][3] = 0.0f;
m[2][0] = 0.0f; m[2][1] = 0.0f; m[2][2] = 1.0f; m[2][3] = 0.0f;
m[3][0] = 0.0f; m[3][1] = 0.0f; m[3][2] = 0.0f; m[3][3] = 1.0f;
}
inline Matrix4f operator*(const Matrix4f& Right) const
{
Matrix4f Ret;
for (unsigned int i = 0; i < 4; i++) {
for (unsigned int j = 0; j < 4; j++) {
Ret.m[i][j] = m[i][0] * Right.m[0][j] +
m[i][1] * Right.m[1][j] +
m[i][2] * Right.m[2][j] +
m[i][3] * Right.m[3][j];
}
}
return Ret;
}
};
#endif /* MATH_3D_H */
pipeline.h
#ifndef PIPELINE_H
#define PIPELINE_H
#include "math_3d.h"
class Pipeline
{
public:
Pipeline()
{
m_scale = Vector3f(1.0f, 1.0f, 1.0f);
m_worldPos = Vector3f(0.0f, 0.0f, 0.0f);
m_rotateInfo = Vector3f(0.0f, 0.0f, 0.0f);
}
void Scale(float ScaleX, float ScaleY, float ScaleZ)
{
m_scale.x = ScaleX;
m_scale.y = ScaleY;
m_scale.z = ScaleZ;
}
void WorldPos(float x, float y, float z)
{
m_worldPos.x = x;
m_worldPos.y = y;
m_worldPos.z = z;
}
void Rotate(float RotateX, float RotateY, float RotateZ)
{
m_rotateInfo.x = RotateX;
m_rotateInfo.y = RotateY;
m_rotateInfo.z = RotateZ;
}
void SetPerspectiveProj(float FOV, float Width, float Height, float zNear, float zFar)
{
m_persProj.FOV = FOV;
m_persProj.Width = Width;
m_persProj.Height = Height;
m_persProj.zNear = zNear;
m_persProj.zFar = zFar;
}
const Matrix4f* GetTrans();
private:
void InitScaleTransform(Matrix4f& m) const;
void InitRotateTransform(Matrix4f& m) const;
void InitTranslationTransform(Matrix4f& m) const;
void InitPerspectiveProj(Matrix4f& m) const;
Vector3f m_scale;
Vector3f m_worldPos;
Vector3f m_rotateInfo;
struct {
float FOV;
float Width;
float Height;
float zNear;
float zFar;
} m_persProj;
Matrix4f m_transformation;
};
#endif /* PIPELINE_H */
pipeline.cpp
#include "pipeline.h"
void Pipeline::InitScaleTransform(Matrix4f& m) const
{
m.m[0][0] = m_scale.x; m.m[0][1] = 0.0f; m.m[0][2] = 0.0f; m.m[0][3] = 0.0f;
m.m[1][0] = 0.0f; m.m[1][1] = m_scale.y; m.m[1][2] = 0.0f; m.m[1][3] = 0.0f;
m.m[2][0] = 0.0f; m.m[2][1] = 0.0f; m.m[2][2] = m_scale.z; m.m[2][3] = 0.0f;
m.m[3][0] = 0.0f; m.m[3][1] = 0.0f; m.m[3][2] = 0.0f; m.m[3][3] = 1.0f;
}
void Pipeline::InitRotateTransform(Matrix4f& m) const
{
Matrix4f rx, ry, rz;
const float x = ToRadian(m_rotateInfo.x);
const float y = ToRadian(m_rotateInfo.y);
const float z = ToRadian(m_rotateInfo.z);
rx.m[0][0] = 1.0f; rx.m[0][1] = 0.0f; rx.m[0][2] = 0.0f; rx.m[0][3] = 0.0f;
rx.m[1][0] = 0.0f; rx.m[1][1] = cosf(x); rx.m[1][2] = -sinf(x); rx.m[1][3] = 0.0f;
rx.m[2][0] = 0.0f; rx.m[2][1] = sinf(x); rx.m[2][2] = cosf(x); rx.m[2][3] = 0.0f;
rx.m[3][0] = 0.0f; rx.m[3][1] = 0.0f; rx.m[3][2] = 0.0f; rx.m[3][3] = 1.0f;
ry.m[0][0] = cosf(y); ry.m[0][1] = 0.0f; ry.m[0][2] = -sinf(y); ry.m[0][3] = 0.0f;
ry.m[1][0] = 0.0f; ry.m[1][1] = 1.0f; ry.m[1][2] = 0.0f; ry.m[1][3] = 0.0f;
ry.m[2][0] = sinf(y); ry.m[2][1] = 0.0f; ry.m[2][2] = cosf(y); ry.m[2][3] = 0.0f;
ry.m[3][0] = 0.0f; ry.m[3][1] = 0.0f; ry.m[3][2] = 0.0f; ry.m[3][3] = 1.0f;
rz.m[0][0] = cosf(z); rz.m[0][1] = -sinf(z); rz.m[0][2] = 0.0f; rz.m[0][3] = 0.0f;
rz.m[1][0] = sinf(z); rz.m[1][1] = cosf(z); rz.m[1][2] = 0.0f; rz.m[1][3] = 0.0f;
rz.m[2][0] = 0.0f; rz.m[2][1] = 0.0f; rz.m[2][2] = 1.0f; rz.m[2][3] = 0.0f;
rz.m[3][0] = 0.0f; rz.m[3][1] = 0.0f; rz.m[3][2] = 0.0f; rz.m[3][3] = 1.0f;
m = rz * ry * rx;
}
void Pipeline::InitTranslationTransform(Matrix4f& m) const
{
m.m[0][0] = 1.0f; m.m[0][1] = 0.0f; m.m[0][2] = 0.0f; m.m[0][3] = m_worldPos.x;
m.m[1][0] = 0.0f; m.m[1][1] = 1.0f; m.m[1][2] = 0.0f; m.m[1][3] = m_worldPos.y;
m.m[2][0] = 0.0f; m.m[2][1] = 0.0f; m.m[2][2] = 1.0f; m.m[2][3] = m_worldPos.z;
m.m[3][0] = 0.0f; m.m[3][1] = 0.0f; m.m[3][2] = 0.0f; m.m[3][3] = 1.0f;
}
void Pipeline::InitPerspectiveProj(Matrix4f& m) const
{
const float ar = m_persProj.Width / m_persProj.Height;
const float zNear = m_persProj.zNear;
const float zFar = m_persProj.zFar;
const float zRange = zNear - zFar;
const float tanHalfFOV = tanf(ToRadian(m_persProj.FOV / 2.0f));
m.m[0][0] = 1.0f / (tanHalfFOV * ar); m.m[0][1] = 0.0f; m.m[0][2] = 0.0f; m.m[0][3] = 0.0;
m.m[1][0] = 0.0f; m.m[1][1] = 1.0f / tanHalfFOV; m.m[1][2] = 0.0f; m.m[1][3] = 0.0;
m.m[2][0] = 0.0f; m.m[2][1] = 0.0f; m.m[2][2] = (-zNear - zFar) / zRange; m.m[2][3] = 2.0f * zFar*zNear / zRange;
m.m[3][0] = 0.0f; m.m[3][1] = 0.0f; m.m[3][2] = 1.0f; m.m[3][3] = 0.0;
}
const Matrix4f* Pipeline::GetTrans()
{
Matrix4f ScaleTrans, RotateTrans, TranslationTrans, PersProjTrans;
InitScaleTransform(ScaleTrans);
InitRotateTransform(RotateTrans);
InitTranslationTransform(TranslationTrans);
InitPerspectiveProj(PersProjTrans);
m_transformation = PersProjTrans * TranslationTrans * RotateTrans * ScaleTrans;
return &m_transformation;
}
GLUT.cpp
// GLUT.cpp : 定義控制檯應用程式的入口點。
//
#include <stdio.h>
#include <glew.h>
#include <freeglut.h>
#include <assert.h>
#include <math.h>
#include "math_3d.h"
#include "pipeline.h"
#pragma comment( lib, "glew32d.lib" )
#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 300
GLuint VBO;
GLuint IBO;
GLuint gWorldLocation;
static const char* pVS = " \n\
#version 330 \n\
\n\
layout (location = 0) in vec3 Position; \n\
\n\
uniform mat4 gWorld; \n\
\n\
out vec4 Color; \n\
\n\
void main() \n\
{ \n\
gl_Position = gWorld * vec4(Position, 1.0); \n\
Color = vec4(clamp(Position, 0.0, 1.0), 1.0); \n\
}";
static const char* pFS = " \n\
#version 330 \n\
\n\
in vec4 Color; \n\
\n\
out vec4 FragColor; \n\
\n\
void main() \n\
{ \n\
FragColor = Color; \n\
}";
static void RenderSceneCB()
{
glClear(GL_COLOR_BUFFER_BIT);
static float Scale = 0.0f;
Scale += 0.1f;
Pipeline p;
p.Rotate(0.0f, Scale, 0.0f);
p.WorldPos(0.0f, 0.0f, 5.0f);
p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 100.0f);
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetTrans());
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(0);
glutSwapBuffers();
}
static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderSceneCB);
glutIdleFunc(RenderSceneCB);
}
static void CreateVertexBuffer()
{
Vector3f Vertices[4];
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.5773f);
Vertices[1] = Vector3f(0.0f, -1.0f, -1.15475);
Vertices[2] = Vector3f(1.0f, -1.0f, 0.5773f);
Vertices[3] = Vector3f(0.0f, 1.0f, 0.0f);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}
static void CreateIndexBuffer()
{
unsigned int Indices[] = { 0, 3, 1,
1, 3, 2,
2, 3, 0,
0, 2, 1 };
glGenBuffers(1, &IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
}
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
GLuint ShaderObj = glCreateShader(ShaderType);
if (ShaderObj == 0) {
fprintf(stderr, "Error creating shader type %d\n", ShaderType);
exit(0);
}
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0] = strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);
GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar InfoLog[1024];
glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
exit(1);
}
glAttachShader(ShaderProgram, ShaderObj);
}
static void CompileShaders()
{
GLuint ShaderProgram = glCreateProgram();
if (ShaderProgram == 0) {
fprintf(stderr, "Error creating shader program\n");
exit(1);
}
AddShader(ShaderProgram, pVS, GL_VERTEX_SHADER);
AddShader(ShaderProgram, pFS, GL_FRAGMENT_SHADER);
GLint Success = 0;
GLchar ErrorLog[1024] = { 0 };
glLinkProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
exit(1);
}
glValidateProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
if (!Success) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
exit(1);
}
glUseProgram(ShaderProgram);
gWorldLocation = glGetUniformLocation(ShaderProgram, "gWorld");
assert(gWorldLocation != 0xFFFFFFFF);
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
glutInitWindowPosition(100, 100);
glutCreateWindow("Tutorial 12");
InitializeGlutCallbacks();
// Must be done after glut is initialized!
GLenum res = glewInit();
if (res != GLEW_OK) {
fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
return 1;
}
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
CreateVertexBuffer();
CreateIndexBuffer();
CompileShaders();
glutMainLoop();
return 0;
}
執行效果: