这篇教程我们来看一看如何在三维场景中对场景中的物体进行各式各样的变换,并且在保持场景的立体感的情况下将其渲染到屏幕上去!常见的方法是用矩阵表示每一个变换, 之后将它们挨个儿相乘,最后乘以顶点位置产生最后的结果!接下来的每一章都将着重介绍一种转变。 我们先来看一个平移变换,平移变换的任务的将一个对象沿着一个任意长度和方向的向量移动,我们假设你想将下图中左边的三角形移到右边的位置:[

](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011701-300x127.png)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011701.png) 要实现这个效果的一个方法就是提供一个偏移向量(上图中是(1,1))作为一个一致变量传递给着色器,仅仅为每个需要处理的顶点加上这个偏移向量即可。然而,这与我们所想要的将一组矩阵通过相乘得到一个综合矩阵的思想背道而驰。除此之外,在后面的教程中你将看到平移通常并不是第一个变换,所以你得在平移变换前让位置向量乘上代表平移之前的变化矩阵,然后乘上这个平移变换,最后乘上代表平移之后的变换的矩阵。所以说上面的方法太笨拙了。最好的方法是找到一个代表平移的矩阵,让它参与到所有矩阵的乘法运算中。但是你能找到一个矩阵,使得这个矩阵和左图三角形中底部的点(0,0)相乘后得到结果(1,1)吗?事实是你使用 2D 矩阵无法做到这一点,对于(0,0,0)用 3D 矩阵也不能做到这一点!总的来说,我们需要的是一个矩阵 M,给定一个点 P 坐标(x,y,z)和一个向量 V(v1,v2,v3)后,能提供 M*P=P1(x+v1,y+v2,z+v3)。简单的来说就是矩阵 M 将 P 平移到 P+V 的位置。在 P1 中我们可以看到它的每个分量都是来自 P 的一个分量和 V 中与之相对应的分量之和。如果我们将变换矩阵设置为单位矩阵 I,我们会得到如下结果: [![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011702-300x155.jpg)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011702.jpg)我们想修改这个自身变换矩阵使结果变成这样子: [![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011703.jpg)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011703.jpg)如果我们坚持用3x3矩阵好像不可能得到想要的结果,但如果改成4x4矩阵我可以这样得到想要的结果:[![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011704-300x133.jpg)
](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011701-300x127.png)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011701.png) 要实现这个效果的一个方法就是提供一个偏移向量(上图中是(1,1))作为一个一致变量传递给着色器,仅仅为每个需要处理的顶点加上这个偏移向量即可。然而,这与我们所想要的将一组矩阵通过相乘得到一个综合矩阵的思想背道而驰。除此之外,在后面的教程中你将看到平移通常并不是第一个变换,所以你得在平移变换前让位置向量乘上代表平移之前的变化矩阵,然后乘上这个平移变换,最后乘上代表平移之后的变换的矩阵。所以说上面的方法太笨拙了。最好的方法是找到一个代表平移的矩阵,让它参与到所有矩阵的乘法运算中。但是你能找到一个矩阵,使得这个矩阵和左图三角形中底部的点(0,0)相乘后得到结果(1,1)吗?事实是你使用 2D 矩阵无法做到这一点,对于(0,0,0)用 3D 矩阵也不能做到这一点!总的来说,我们需要的是一个矩阵 M,给定一个点 P 坐标(x,y,z)和一个向量 V(v1,v2,v3)后,能提供 M*P=P1(x+v1,y+v2,z+v3)。简单的来说就是矩阵 M 将 P 平移到 P+V 的位置。在 P1 中我们可以看到它的每个分量都是来自 P 的一个分量和 V 中与之相对应的分量之和。如果我们将变换矩阵设置为单位矩阵 I,我们会得到如下结果: [![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011702-300x155.jpg)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011702.jpg)我们想修改这个自身变换矩阵使结果变成这样子: [![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011703.jpg)](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011703.jpg)如果我们坚持用3x3矩阵好像不可能得到想要的结果,但如果改成4x4矩阵我可以这样得到想要的结果:[![](http://www.wjgbaby.com/wp-content/uploads/2018/01/18011704-300x133.jpg)
这样使用一个4维向量表示一个3维向量叫做齐次坐标,这在3d图形学中很常用也很有用,第四个分量称作“w”。事实上,我们之前教程中看到的内部shader符号变量gl_Position就是一个4维向量,第四个分量“w”在从3d到2d的投影变换中起着关键作用。通常对于表示点的矩阵会让w=1,而对于表示向量的矩阵会让w=0,因为点可以被做变换而向量不可以,你可以改变一个向量的长度和方向,但是长度和方向一样的所有向量都是相等的,不管他们的起点在哪里,所以我们可以把所有的向量起点放到原点来看。对于向量设置w=0然后乘以变换矩阵会得到和自身一样的向量。 1.

structMatrix4f {
float m[4][4];
};

我们将一个 4*4 的矩阵定义添加到 math_3d.h 中。从现在起,它将被运用到大多数转变矩阵中。 2.

GLuintgWorldLocation;

我们使用这个句柄来获取着色器中的世界矩阵一致变量地址!我们称之为“世界”是因为我们所做的是,在我们的虚拟世界坐标系统中将一个物体移动(变换)到我们想要的位置。 3.

Matrix4fWorld;
World.m[0][0] = 1.0f; World.m[0][1] =0.0f; World.m[0][2] = 0.0f; World.m[0][3] = sinf(Scale);
World.m[1][0] = 0.0f; World.m[1][1] =1.0f; World.m[1][2] = 0.0f; World.m[1][3] = 0.0f;
World.m[2][0] = 0.0f; World.m[2][1] =0.0f; World.m[2][2] = 1.0f; World.m[2][3] = 0.0f;
World.m[3][0] = 0.0f; World.m[3][1] =0.0f; World.m[3][2] = 0.0f; World.m[3][3] = 1.0f;

在渲染函数中,我们创建了一个 4*4 矩阵并根据上面的推导对其进行初始化!我们设置 v2 和 v3 为 0,因为我们希望物体在 y、z 坐标上没有变化,我们将 v1 的值设置为 sin 函数的结果,Scale的值在每一帧中都是不断变化的,这使得 X 坐标的值会在 -1 到 1 的范围内波动。现在我们需要把矩阵加载到着色器中。 4.

glUniformMatrix4fv(gWorldLocation,1, GL_TRUE, &World.m[0][0]);

这是另外一个 glUniform _函数的例子,用来加载数据到着色器的一致变量中。这个特定的函数可以加载 4*4 的矩阵,也有用于加载 2*2, 3*3, 3*2, 2*4, 4*2, 3*4 和 4\_3的版本。第一个参数是一致变量的位置(在着色器程序编译之后由 glGetUniformLocation() 函数返回的结果)。第二个参数代表我们更新的矩阵的数量。一个矩阵我们用 1,但是我们也可以调用这个函数一次更新多个矩阵。第三个参数很容易迷惑新手。它表明提供矩阵是按行优先还是列优先的! 关键在于 C/C++ 语言默认就是行优先的!这意味着当你给二维数组填充数据时,它们在内存中一行一行的排列,并且最上面的一行在低地址处!例如,看下面的数组:

int a[2][3];
a[0][0]= 1;
a[0][1]= 2;
a[0][2]= 3;
a[1][0]= 4;
a[1][1]= 5;
a[1][2]= 6;

直观看来上这个数组看起来像下面的矩阵:

1 2 3
4 5 6

而在内存中的排列是这样的:1 2 3 4 5 6(1在最低地址) 所以我们设定函数 glUniformMatrix4fv() 第三个参数是 GL_TRUE 是因为我们以行优先的方式提供矩阵的。我们也可以将第三个参数为 GL_FALSE,但是这样的话我们需要转置矩阵的值,因为 C/C++ 中内存的排列仍然是行优先,但是 OpenGL 将认为我们提供的前四个值实际上是一个矩阵的列,并做相应处理。第四个参数是矩阵在内存的开始地址! 5. 在着色器中的其余代码

Uniform  mat4  gWorld;

这是一个 4*4 的矩阵类型的一致变量。也有 mat2 和 mat3。 6.

gl_Position= gWorld * vec4(Position, 1.0);

我们添加到顶点缓冲区中的三角形顶点的位置属性是一个三维向量,但是之前我们知道对于一个点,其 W 分量应该为 1。所以这里有两种选择:

  • 将填充到缓冲区中的顶点属性都改为四维向量;
  • 或者在顶点着色器中增加第四部分。

第一个选择没有优势,因为每个顶点位置属性需要消耗额外的四字节内存,但是我们知道那部分的内容一直是 1。较之而言,在VBO中维持三个分量的点,之后在着色器中为其添加第四个分量的方法就高效很多。在GLSL中通过使用 ‘vec4(Position, 1.0)’ 完成这个扩充。我们将矩阵与这个顶点向量相乘,最后将其结果传入 gl_Position 中。总之本例中,每一帧我们都生成一个变换矩阵使得对象沿着 X 轴平移,并且这个平移的距离在[-1,1]之间波动。着色器将每个顶点的位置与此矩阵相乘,结果使物体左右移动。在大多数情况下,在顶点着色器完成处理后,三角形的一边会超出规范化空间,这时候裁剪器将把超出的那部分裁剪掉。这样我们就只能看到位于规范化空间内部的部分。 项目代码:

#include <stdio.h>
#include<string.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include “ogldev_math_3d.h” //用于OpenGL的3d数学库
#include “ogldev_util.h” //用于读取文本文件

GLuint VBO; //全局GLuint引用变量,来操作顶点缓冲器对象
GLuint gWorldLocation; // 平移变换一致变量的句柄引用

// 定义要读取的顶点着色器脚本和片断着色器脚本的文件名,作为文件读取路径
const char* pVSFileName = “shader.vs”;
const char* pFSFileName = “shader.fs”;

static void RenderCallBack()
{
glClear(GL_COLOR_BUFFER_BIT); //清空颜色缓存

static float Scale = 0.0f;   //维护一个不断慢慢增大的静态浮点数
Scale += 0.001f;    //如果图像变化太快或者太慢,可调节此数值

// 4x4的平移变换矩阵
Matrix4f World;

World.m\[0\]\[0\] = 1.0f; World.m\[0\]\[1\] = 0.0f; World.m\[0\]\[2\] = 0.0f; World.m\[0\]\[3\] = sinf(Scale);
World.m\[1\]\[0\] = 0.0f; World.m\[1\]\[1\] = 1.0f; World.m\[1\]\[2\] = 0.0f; World.m\[1\]\[3\] = 0.0f;
World.m\[2\]\[0\] = 0.0f; World.m\[2\]\[1\] = 0.0f; World.m\[2\]\[2\] = 1.0f; World.m\[2\]\[3\] = 0.0f;
World.m\[3\]\[0\] = 0.0f; World.m\[3\]\[1\] = 0.0f; World.m\[3\]\[2\] = 0.0f; World.m\[3\]\[3\] = 1.0f;

// 将矩阵数据加载到shader中
glUniformMatrix4fv(gWorldLocation, 1, GL\_TRUE, &World.m\[0\]\[0\]);

glEnableVertexAttribArray(0);   //开启顶点属性
glBindBuffer(GL\_ARRAY\_BUFFER, VBO);   //绑定GL\_ARRAY\_BUFFER缓冲器
glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 0, 0);   //管线解析bufer中的数据

glDrawArrays(GL\_TRIANGLES, 0, 3);   //画三角形,3个顶点
glDisableVertexAttribArray(0);   //禁用顶点数据
glutSwapBuffers();   //交换前后缓存

}

static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderCallBack);
glutIdleFunc(RenderCallBack); //将渲染回调注册为全局闲置回调
}

static void CreateVertexBuffer()
{
Vector3f Vertices[3]; //创建含有3个顶点的顶点数组
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
Vertices[1] = Vector3f(1.0f, -1.0f, 0.0f);
Vertices[2] = Vector3f(0.0f, 1.0f, 0.0f);

glGenBuffers(1, &VBO);   //创建缓冲器
glBindBuffer(GL\_ARRAY\_BUFFER, VBO);   //绑定GL\_ARRAY\_BUFFER缓冲器

glBufferData(GL\_ARRAY\_BUFFER, sizeof(Vertices), Vertices, GL\_STATIC\_DRAW);   //绑定顶点数据

}

//使用shader文本编译shader对象,并绑定shader到着色器程序中
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
// 根据shader类型参数定义两个shader对象
GLuint ShaderObj = glCreateShader(ShaderType);
// 检查是否定义成功
if (ShaderObj == 0)
{
fprintf(stderr, “Error creating shader type %d\n”, ShaderType);
exit(0);
}

// 定义shader的代码源
const GLchar\* p\[1\];
p\[0\] = pShaderText;
GLint Lengths\[1\];
Lengths\[0\] = strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);    // 编译shader对象

// 检查和shader相关的错误
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);
}

// 将编译好的shader对象绑定到program object程序对象上
glAttachShader(ShaderProgram, ShaderObj);

}

// 编译着色器函数
static void CompileShaders()
{
// 创建着色器程序
GLuint ShaderProgram = glCreateProgram();
// 检查是否创建成功
if (ShaderProgram == 0)
{
fprintf(stderr, “Error creating shader program\n”);
exit(1);
}

// 存储着色器文本的字符串缓冲
string vs, fs;

// 分别读取着色器文件中的文本到字符串缓冲区
if (!ReadFile(pVSFileName, vs))
{
    exit(1);
};
if (!ReadFile(pFSFileName, fs))
{
    exit(1);
};

// 添加顶点着色器和片段着色器
AddShader(ShaderProgram, vs.c\_str(), GL\_VERTEX\_SHADER);
AddShader(ShaderProgram, fs.c\_str(), GL\_FRAGMENT\_SHADER);

// 链接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);
}

// 设置到管线声明中来使用上面成功建立的shader程序
glUseProgram(ShaderProgram);

// 查询获取一致变量的位置
gWorldLocation = glGetUniformLocation(ShaderProgram, "gWorld");
assert(gWorldLocation != 0xFFFFFFFF);    // 检查错误

}

int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("平移变换");

glutDisplayFunc(RenderCallBack);  //开始渲染

InitializeGlutCallbacks();

// 检查GLEW是否就绪,必须要在GLUT初始化之后!
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(); //创建顶点缓冲器

CompileShaders();   // 编译着色器

glutMainLoop();   //通知开始GLUT的内部循环

return 0;

}

片断着色器shader.fs脚本代码:

#version 330 //告诉编译器我们的目标GLSL编译器版本是3.3

out vec4 FragColor; // 片段着色器的输出颜色变量

// 着色器的唯一入口函数
void main()
{
// 定义输出颜色值
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

顶点着色器shader.vs脚本代码:

#version 330 //告诉编译器我们的目标GLSL编译器版本是3.3

// 绑定定点属性名和属性,方式二缓冲属性和shader属性对应映射
layout (location = 0) in vec3 Position;

// 平移变换聚矩阵一致变量
uniform mat4 gWorld;

void main()
{
// 用平移变换矩阵乘以图形顶点位置对应的4X4矩阵相乘,完成平移变换
gl_Position = gWorld * vec4(Position, 1.0);
}

运行效果(gif图): 参考链接: http://ogldev.atspace.co.uk/www/tutorial06/tutorial06.html https://jiangxh1992.github.io/articles/2016-09/openglstepbystep6