OpenGL学习笔记(三)-变换-坐标系统-摄像机

参考网址:LearnOpenGL 中文版
哔哩哔哩教程

1.5 变换

为了使物体变成动态的,需要使用多个矩阵对象变换一个物体。

1.5.1 向量

向量包含方向和模。由于向量是一个方向,很难用位置表示,因此可指定原点,然后指向一个方向,对应一个点,使其变为位置向量。

1、向量与标量运算:数学中是没有这个运算的,但是在许多线性代数的库中支持这样的运算。该运算就是将向量的每个元素,与标量进行运算。其中的+可以是+,-,·或÷。注意-和÷运算时不能颠倒(标量-/÷向量),因为颠倒的运算是没有定义的。
在这里插入图片描述
2、向量取反:向量前面加上负号,使得方向逆转。
3、向量加减:对应分量相加减,其几何意义为:
在这里插入图片描述在这里插入图片描述
4、向量点乘,两个向量点乘是对应分量相乘再相加,结果是一个标量,同时也等于两个两个向量的模相乘再乘以余弦值,通过下面两个式子,可以通过点乘计算两个向量的夹角,这可用于光照的计算:


5、向量叉乘:两个不平行向量的叉乘结果,为一个正交于两个输入向量的向量


6、在GLSL中,还定义了两个向量的逐元素乘法(componet wise),vec4 c=a*b,结果仍为一个向量。

1.5.2 矩阵

1、矩阵与标量的加减,就是每个分量与标量做运算,同样在数学中是不存在的。
2、矩阵与标量数乘,每个分量相乘。标量用它的值缩放(Scale)矩阵的所有元素。
3、矩阵与向量相乘:向量变为齐次坐标,增加w分量,w分量一般为1,主要用于向量位移。

  • 缩放,对向量长度进行更改
    在这里插入图片描述
  • 位移在这里插入图片描述
  • 使用齐次坐标可以允许我们在3D向量上进行位移。如果一个向量w的齐次坐标是0,这个坐标就是方向向量(Direction Vector),这个向量就不能位移(不能位移一个方向)。
  • 旋转,沿x轴旋转
    在这里插入图片描述
    1.5.3 实践

1、下载GLM库进行矩阵运算,链接,该库只有头文件,需在vs包含目录里添加对应的头文件文件夹,在项目里包含头文件:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

2、定义矩阵

  • 0.9.9版本以前的GLM,默认会将矩阵类型初始化为单位矩阵。
  • 按顺序定义位移、旋转和缩放矩阵,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵,即trans=TranslateMat*RotateMat*ScaleMat
  • 对上述矩阵的理解顺序,如果绕着固定坐标系变换,则上述变换为左乘,先缩放,再绕正坐标系Z轴旋转,再平移。
  • 即实际的变换顺序应该与阅读顺序相反:尽管在代码中我们先位移再旋转,实际的变换却是先应用旋转再是位移的。
  • 平移矩阵中,定义三维方向向量 glm::vec3(1.0f, 1.0f, 0.0f)表示平移,注意这里不是齐次矩阵,只有定义顶点数据时,才利用齐次矩阵。
  • 在旋转矩阵中,利用glm::radians(0.1f)将度数转为弧度。
glm::mat4 trans;//单位矩阵
trans = glm::translate(trans, glm::vec3(0, 0, 1.0));
trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0, 0, 1.0f));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

3、如何把矩阵传递给着色器?在GLSL里也有一个mat4类型,所以修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

#version 330 core									
layout(location = 0) in vec3 aPos;					
layout(location = 1) in vec3 aColor;	
layout(location = 2) in vec2 aTexCoord;		
	
out vec3 vertexColor;
out vec2 TexCoord;
	
uniform mat4 tansform;
					
void main()											
{													
	gl_Position =tansform*vec4(aPos.x, aPos.y, aPos.z, 1.0);
	vertexColor=aColor;		
	TexCoord=aTexCoord;						
}

4、将矩阵传递给着色器。

  • 用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。第一个参数是uniform的位置值。第二个参数为矩阵数量。第三个参数为是否需要转置。最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

在这里插入图片描述

1.6 坐标系统

1.6.1 概述

OpenGL在顶点着色器中会将范围内的坐标变换为标准化设备坐标(-1.0到1.0之间),然后传入光栅器(Rasterizer),变换为屏幕上的二维坐标或像素。
在上述过程中,顶点会经过几个变换矩阵被变换到多个坐标系统。顶点坐标起始于局部坐标,经模型矩阵后变为世界坐标,经观察矩阵后变成观察坐标,经投影矩阵后变成裁剪坐标,并最后以屏幕坐标的形式结束。
在这里插入图片描述
1、局部坐标:某个物体相对于自身局部坐标系原点的坐标。

2、世界坐标:全部物体摆放于世界空间中,某个物体相对于世界坐标系原点的坐标。利用模型矩阵,可以将物体模型放置到场景中的某个位置。

3、观察坐标:某个物体相对于摄像机坐标系原点的坐标。

4、裁剪坐标:为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵会将范围内的坐标变换为标准化设备坐标。投影矩阵分为正射投影矩阵(Orthographic Projection Matrix)和透视投影矩阵(Perspective Projection Matrix),由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum)。

5、屏幕坐标: 所有顶点被变换到裁剪空间后,会自动执行透视除法(Perspective Division),将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在顶点着色器运行的最后自动执行。然后OpenGL会通过glViewport函数将坐标映射到屏幕坐标。

1.6.2 投影矩阵

1、 正射投影矩阵定义了一个类似立方体的平截头箱,创建一个正射投影矩阵需要指定宽、高、近(Near)平面和远(Far)平面。正射平截头体直接将坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变,这也导致正射投影矩阵在屏幕上产生的物体没有远近之分,效果不真实。

//创建正射投影矩阵,分别对应左右坐标,底部顶部坐标,近平面和远平面
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

在这里插入图片描述
2、透视投影矩阵:在真实世界中,离得越远的物体看起来更小,两条线在很远的地方会相交。为模仿这样的效果,使用透视投影矩阵。
在这里插入图片描述

  • 透视投影矩阵将给定的平截头体范围映射到裁剪空间,并且修改顶点坐标的w值,使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间。这也就使得在透视除法时,顶点坐标的每个分量都除以它的w分量,距离观察者越远顶点坐标就会越小。

  • 透视平截头体可以被看作一个不均匀形状的箱子。

    //定义透视投影矩阵,第一个参数为视角大小,通常为45;
    //第二个参数为宽高比,由视口的宽除以高
    //第三四参数为近远平面,一般为0.1与100
    glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
    

    在这里插入图片描述
    在这里插入图片描述

3、投射投影矩阵如何将观察坐标变换为裁剪坐标?

  • 在透视投影中,投影矩阵对顶点进行了缩放。以4个关键点经过投影矩阵变换后的结果,推导透视投影矩阵。图源《Unity Shader 入门精要》

在这里插入图片描述

  • 对比上图四个顶点变换前后的值,推导变换矩阵。
  • 从侧视图推导Y方向的变换,变换前近平面的Y坐标为A,远平面的Y坐标为B,变换后近平面的Y坐标为N,远平面的Y坐标为F;
  • 从俯视图推导X方向的变换,变换前近平面的X坐标为C,远平面的Y坐标为D,变换后近平面的X坐标为N,远平面的X坐标为F;
  • 设Z方向的变换为一次线性变换,推导Z方向的变换,变换前近平面的Z坐标为-N,远平面的Z坐标为-F,变换后近平面的Z坐标为-N,远平面的Z坐标为F;
  • W分量可直接由1变换为Z分量的值,就可以使得远的物体看起来小,但是由于观察坐标是右手坐标系,NDC坐标是左手坐标系,所以W分量为-Z。

在这里插入图片描述
变换矩阵则为:
在这里插入图片描述
1.6.3 进入3D

1、3D绘图,定义以上三个矩阵,使得模型进入3D

  • 定义模型矩阵,将其绕着x轴旋转,使它看起来像把模型放在地上一样。
glm::mat4 modelMat;
modelMat = glm::rotate(modelMat, glm::radians(-55.0f), glm::vec3(1.0f, 0, 0));
  • 定义观察矩阵,观察矩阵以相反于摄像机移动的方向移动整个场景。若想要摄像机往后移动(即沿着z轴的正方向移动),则需要将场景沿着z轴负方向平移来实现。
glm::mat4 viewMat;
viewMat = glm::translate(viewMat, glm::vec3(0, 0, -3.0f));
  • 透视投影矩阵
glm::mat4 projMat;
projMat = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
  • 在顶点着色器中运用变换,按照左乘的顺序
#version 330 core									
layout(location = 0) in vec3 aPos;					
layout(location = 1) in vec3 aColor;	
layout(location = 2) in vec2 aTexCoord;		
	
out vec3 vertexColor;
out vec2 TexCoord;
	
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
					
void main()											
{													
	gl_Position =projMat*viewMat*modelMat*vec4(aPos.x, aPos.y, aPos.z, 1.0);
	vertexColor=aColor;		
	TexCoord=aTexCoord;						
}
  • 将矩阵传入uniform矩阵变量
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"),1,GL_FALSE,glm::value_ptr(modelMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

在这里插入图片描述
2、绘制一个立方体

  • 传入36个顶点数据,绘制立方体,数据

  • 由于顶点数据中没有颜色数据,注意更改属性读取方式。

  • 由于没有定义顶点索引,无法使用EBO的glDrawElement()绘画,因此改用glDrawArrays(GL_TRIANGLES, 0, 36);

  • 更改模型矩阵,使得旋转矩阵随着时间变化,立方体随着时间旋转

    modelMat = glm::rotate(modelMat, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
    
  • 设置Z缓冲(深度缓冲),没有开启深度缓冲时,Opengl在渲染时可能会覆盖已有的片段,使得原本看不见的片段会渲染出来。因此GLFW会自动生成一个缓冲,类似于深度图片,将该片段的深度值映射为灰度值,然后储存到图像中,越靠前灰度值越大。

  • 当新的片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,则灰度值较小,它将会被丢弃。
    在这里插入图片描述
    在这里插入图片描述

    glEnable(GL_DEPTH_TEST);//开启深度测试
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//渲染前清除缓冲
    

在这里插入图片描述
3、创建多个立方体

  • 在项目中只创建一个立方体,但在渲染时调用gldrawarrays10次,每次传入一个不同的模型矩阵给顶点着色器,使得模型在场景中的位置不同。
glm::vec3 cubePositions[] = {
  glm::vec3( 0.0f,  0.0f,  0.0f), 
  glm::vec3( 2.0f,  5.0f, -15.0f), 
  glm::vec3(-1.5f, -2.2f, -2.5f),  
  glm::vec3(-3.8f, -2.0f, -12.3f),  
  glm::vec3( 2.4f, -0.4f, -3.5f),  
  glm::vec3(-1.7f,  3.0f, -7.5f),  
  glm::vec3( 1.3f, -2.0f, -2.5f),  
  glm::vec3( 1.5f,  2.0f, -2.5f), 
  glm::vec3( 1.5f,  0.2f, -1.5f), 
  glm::vec3(-1.3f,  1.0f, -1.5f)  
};
myShader->use();
glBindVertexArray(VAO);

for (size_t i = 0; i < 10; i++)
{
	glm::mat4 modelMat;
	modelMat = glm::translate(modelMat, cubePositions[i]);
	float angle = 20.0f*(i+1);
	modelMat = glm::rotate(modelMat, (float)glfwGetTime()*glm::radians(angle), glm::vec3(0.5f, 1.0f, 0.0f));
	glm::mat4 viewMat;
	viewMat = glm::translate(viewMat, glm::vec3(0, 0, -3.0f));
	glm::mat4 projMat;
	projMat = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);


	glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
	glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
	glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

	glDrawArrays(GL_TRIANGLES, 0, 36);
}

在这里插入图片描述

1.7 摄像机

本节主要内容:定义一个摄像机类,通过改变观察矩阵,移动旋转场景。

1.7.1 摄像机坐标系/观察空间

1、定义一个摄像机,需要它在世界空间中的位置、三个轴的方向向量。

在这里插入图片描述

  • 摄像机位置就是世界空间中一个指向摄像机位置的向量Position

  • 摄像机方向(Z轴方向Front):摄像机指向场景原点(0, 0, 0),用场景原点向量减去摄像机位置向量的就是摄像机的指向向量,此时摄像机指向世界坐标系z轴的负方向。

  • 摄像机右轴(X轴方向Right):在摄像机坐标系变换中,设定Z轴固定,X-Y面不发生旋转。因此将Z轴方向向量与世界坐标系Y轴方向向量(向上向量WorldUp)进行叉乘,得到指向x轴正方向向量。

  • 摄像机上轴(Y轴方向Up):将Z轴方向与X轴方向进行叉乘。

2、通过构造函数创建一个摄像机对象:

Camera::Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup)
{
	Position = position;
	Worldup = worldup;
	Pitch = pitch;
	Yaw = yaw;
	Front.x = cos(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Front.y = sin(glm::radians(Pitch));
	Front.z = sin(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Right = glm::normalize(glm::cross(Front, Worldup));
	Up = glm::normalize(glm::cross(Right, Front));
}

3、通过LookAt函数创建观察矩阵,在矩阵中位置向量是相反的,因为需把世界平移到与我们自身移动的相反方向。
在这里插入图片描述
LookAt函数需定义摄像机位置Position,目标位置target=Position+Front和世界空间中的上向量WorldUp

glm::mat4 Camera::GetViewMatrix()
{
	return glm::lookAt(Position, Position + Front, Worldup);
}

4、定义摄像机对象,创建观察矩阵

Camera camera(glm::vec3(0, 0, 3.0f), glm::vec3(0, -1.0f, 0), glm::vec3(0, 1.0f, 0));

glm::mat4 viewMat;
viewMat = camera.GetViewMatrix();

1.7.2 欧拉角

1、用欧拉角对场景进行旋转,欧拉角是表示3D空间中任何旋转的3个值,一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll)
在这里插入图片描述
2、在摄像机坐标系变换中,设定Z轴固定,X-Y面不发生旋转,即滚转角是不变的。通过俯仰角和偏航角,计算摄像机的方向向量,即Z轴方向Front,其三个分量的计算方式如下图所示:
在这里插入图片描述

Camera::Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup)
{
	Position = position;
	Worldup = worldup;
	Pitch = pitch;
	Yaw = yaw;
	Front.x = cos(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Front.y = sin(glm::radians(Pitch));
	Front.z = sin(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Right = glm::normalize(glm::cross(Front, Worldup));
	Up = glm::normalize(glm::cross(Right, Front));
}

3、定义摄像机对象,创建观察矩阵,注意当俯仰角和偏航角都为0时,摄像机方向是指向X轴正方向的,因此需要设偏航角为-90,摄像机方向才能指向z轴负方向,与上述一致。

Camera camera(glm::vec3(0, 0, 3.0f), 0.0f, -90.0f, glm::vec3(0, 1.0f, 0));

1.7.3 鼠标输入

偏航角和俯仰角是通过鼠标移动获得的,水平的移动影响偏航角,竖直的移动影响俯仰角。它的原理就是,储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大,也就是摄像机需要移动更多的距离。

1、利用GLFW隐藏光标,并捕捉(Capture)它

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

2、为了计算俯仰角和偏航角,让GLFW监听鼠标移动事件,并用一个回调函数改变获得鼠标的偏移量,通过偏移量改变欧拉角。

void mouse_callback(GLFWwindow* window,double xPos,double yPos)
glfwSetCursorPosCallback(window, mouse_callback);
void mouse_callback(GLFWwindow* window,double xPos,double yPos)
{
	if (firstMouse == true)
	{
		lastX = xPos;
		lastY = yPos;
		firstMouse = false;
	}
	
	float deltaX, deltaY;
	deltaX = xPos - lastX;
	deltaY = yPos - lastY;

	lastX = xPos;
	lastY = yPos;

	camera.ProcessMosueMovement(deltaX, deltaY );

}
void Camera::ProcessMosueMovement(float deltaX, float deltaY)
{
	Pitch += deltaY*SenseY;
	Yaw -= deltaX*SenseX;
	UpdateCameraVectors();
}

3、改变欧拉角,改变摄像机坐标轴的方向向量

void Camera::ProcessMosueMovement(float deltaX, float deltaY)
{
	Pitch += deltaY*SenseY;
	Yaw -= deltaX*SenseX;
	UpdateCameraVectors();
}


void Camera::UpdateCameraVectors()
{
	Front.x = cos(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Front.y = sin(glm::radians(Pitch));
	Front.z = sin(glm::radians(Yaw))*cos(glm::radians(Pitch));
	Right = glm::normalize(glm::cross(Front, Worldup));
	Up = glm::normalize(glm::cross(Right, Front));
}

1.7.3 键盘输入

通过之前的键盘输入函数,当我们按下WS键,摄像机的位置向前或向后移动,将位置向量加上或减去方向向量,即摄像机位置沿着其z轴方向移动。

void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
	{
		camera.ProcessZMovement(0.01f);//向z轴正方向
	}
	else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
	{
		camera.ProcessZMovement(-0.01f);
	}
}
void Camera::ProcessZMovement(float deltaZ)
{
	Position += Front*deltaZ*0.1f;
}

Video_01

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值