OGL(教程12)——透视投影

原文地址:http://ogldev.atspace.co.uk/www/tutorial12/tutorial12.html

背景知识:
我们最终来到了最重要的一节,把3D世界映射到2D平面,这个映射还要保留深度信息。一个很好的例子是,铁路轨道的图片,在很远的地方两个轨道讲汇聚于一点。
如图:
在这里插入图片描述

我们准备推导一个变换以能够给满足上面的需求,我们还有一个需求,使用这个变换的时候,同时也把裁剪工作做了。我们把坐标映射到-1到1之间。也就是说裁剪器不需要知道屏幕的维度,也不需要知道近平面和远平面就能做裁剪工作。

透视变换需要我们提供四个参数:

  1. 宽高比——最终投射的矩形区域的宽度和高度的比例
  2. 垂直视野——摄像机的张角
  3. 近平面的坐标——距离摄像机距离小于这个近平面的则裁剪掉
  4. 远平面的坐标——距离摄像机大于这个远平面的物体会被裁剪掉

宽高比是需要的,由于我们需要使用规格化空间的坐标,这个宽度和高度相同。由于很少遇到屏幕的宽度比高要大,也就是说顶点在宽度上要进行很大的压缩。这使我们能够在宽度上看到更多的像素。

垂直视野允许我们能够放大或者缩小世界。看下面的例子,在左图中,张角比较大,这使物体看起来比较小,而右图中张角比较下,这会使物体看起来比较大。但是这个和摄像机的位置有关系。

在这里插入图片描述

我们定义投射面和摄像机的距离。投射面是和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值,然后做平移处理。
f(z)=A.z +B

对上面的等式右边做透视除法:
A+

我们需要找到A和B值,使其映射到[-1,1]之间。我们知道z的最小值为-1的时候,对应的是近平面NearZ,而1对应的是远平面FarZ,因此我们可以得出:
在这里插入图片描述

然后我们选择矩阵的第三行(abcd),使其满足:
a.

我们可以立即将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;
}

运行效果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值