坐标与变换

150 阅读8分钟
title: 坐标与变换
date: 2024-12-23 14:30:00
categories:
- OpenGL
- Cherno
tags: D8

变换

知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,接下来可以给物体添加变换,使其每帧变换

四元数

四元数就是一种用来表示三维旋转的数学工具,跟我们平时用的角度和矩阵不太一样。

为什么用四元数?

  • 避免万向节锁:用欧拉角表示旋转时,有时会遇到奇怪的锁死现象,四元数没有这个问题。
  • 高效:计算量比旋转矩阵小,运算更快。
  • 平滑插值:四元数可以做更平滑的旋转过渡,适合做动画和3D游戏中的旋转效果。

四元数的基本结构

一个四元数通常是这样表示的:

q=w+xi+yj+zkq = w + xi + yj + zkq=w+xi+yj+zk

  • www 是实数部分,决定旋转的大小。
  • x,y,zx, y, zx,y,z 是虚数部分,表示旋转的轴。

用四元数做旋转

  • 你可以把三维空间中的旋转看作是一个四元数。
  • 旋转时,四元数可以直接作用在一个点上,改变它的位置和方向。

简单的例子:

假设我们有一个绕某轴旋转的动作,可以用四元数表示它。旋转过后,物体的方向会发生变化。

总结:

四元数主要用于表示和计算三维空间中的旋转,比传统的角度和矩阵方法更高效,特别适合需要大量旋转计算的场景,比如3D动画、游戏开发等。

GLM

GLM是一个只有头文件的库,GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)如果使用的是0.9.9或0.9.9以上的版本,你需要将所有的矩阵初始化改为 glm::mat4 mat = glm::mat4(1.0f)

需要用到的头文件

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

使用实例

把一个向量(1, 0, 0)位移(1, 1, 0)个单位

glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
// 译注:下面就是矩阵初始化的一个例子,如果使用的是0.9.9及以上版本
// 下面这行代码就需要改为:
// glm::mat4 trans = glm::mat4(1.0f)
// 之后将不再进行提示
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;

矩阵的计算中,每次对矩阵乘的向量/矩阵都会被应用到被乘矩阵中,所以最终的矩阵会有所有参与计算的性质。

如何将矩阵传给shader

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
​
out vec2 TexCoord;
​
uniform mat4 transform;
​
void main()
{
    gl_Position = transform * vec4(aPos, 1.0f);
    TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}

GLSL也有mat2mat3类型从而允许了像向量一样的混合运算

并通过glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));来setuniform。

void glUniformMatrix4fv(    GLint location,//uniform的位置值
GLsizei count,                           //告诉OpenGL将要发送多少个矩阵
GLboolean transpose,                      //是否希望对我们的矩阵进行转置
const GLfloat *value);                    //矩阵数据(value_ptr)

坐标系统

OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的xyz坐标都应该在**-1.01.0**之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。

coordinate_systems

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

局部空间

物体本身的空间,物体本身的一个坐标空间

世界空间

物体所在的空间,物体在世界中的变换,(Model Matrix)实现,包括位移,旋转,缩放。

裁剪空间

在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。

由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

正射投影

创建:glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

orthographic projection frustum

透视投影

创建: glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

 perspective_frustum

第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的平面,通常设置近距离为0.1f,而远距离设为100.0f,近平面和远平面内且处于平截头体内的顶点都会被渲染。

组合

V = P * V * M * Local

模型矩阵、观察矩阵和投影矩阵,矩阵的计算是相反的,从右向左,最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

然后呢?

顶点着色器的输出要求所有的顶点都在裁剪空间内,这正是我们刚才使用变换矩阵所做的。OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换。

Z缓冲(深度缓冲)

GLFW会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的

glEnable(GL_DEPTH_TEST);

因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,我们可以通过在glClear函数中指定DEPTH_BUFFER_BIT位来清除深度缓冲:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);