OpenGL3.3+GLFW+GLEW+GLM实现小人行走动画

现在在网上找比较系统的教程基本都是旧版OpenGL,都是基于glut的,然而这个库已经是上个世纪的了……新版的OpenGL3.3及以上的教程,强烈推荐以下这两个:

openGL tutorial http://www.opengl-tutorial.org/cn/

learn openGL https://learnopengl-cn.github.io

两份教程一个侧重理解,一个侧重代码实现,可以穿插着一起看。

另外非常感谢xuhongxu同学。

本文原发于这里,是图形学课的一次实验。

1、环境

  • OpenGL 3.3

  • GLFW,用于创建窗口和处理用户输入

  • GLEW,用于确定OpenGL函数的具体实现

  • GLM,图像相关的数学计算工具库

  • OSX 10.10

2、窗口和库的初始化

GLFW窗口的初始化:

if(!glfwInit())
{
    return -1;
}
GLFWwindow* window;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//OpenGL版本为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//最低兼容版本
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用新版本的核心模式
glfwWindowHint(GLFW_RESIZABLE , GL_FALSE);//窗口尺寸不可变
glfwWindowHint(GLFW_SAMPLES, 4);
window = glfwCreateWindow(WIDTH, HEIGHT, "Hello World", NULL, NULL);
if (!window)
{
    glfwTerminate();
    return -1;
}
glfwMakeContextCurrent(window);//设为当前窗口
glfwSetKeyCallback(window, key_callback);

GLEW库的初始化:

glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
    glfwTerminate();
    return -1;
}

3、着色器

OpenGL3.3及以上版本不再使用立即渲染模式,采用核心模式,着色器使用的语言是GLSL。核心模式的工作流程为如下图所示图形渲染管线,处理过程中一步步将3D的图形渲染成屏幕上显示的2D图像。在实验中,需要定义自己的顶点着色器(vertex shader)和片段着色器(fragment shader)。

顶点着色器定义如下:

#version 330 core

//第一层缓冲,编号为0,从C++输入数据,顶点位置
layout(location = 0) in vec3 vertexPosition_modelspace;

//第二层缓冲,编号为1,从C++输入数据,顶点颜色
layout(location = 1) in vec3 vertexColor;

//输出数据, 给片段着色器
out vec4 fragmentColor;

//全局数据,MVP矩阵
uniform mat4 MVP;

void main(){    

    //顶点位置,参数
    gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

    //片段颜色,传递给片段着色器
    fragmentColor.xyz = vertexColor;
    fragmentColor.a=0.9;
}

片段着色器定义如下:

#version 330 core

//输入,来自顶点着色器的颜色
in vec4 fragmentColor;

//输出
out vec4 color;

void main(){

    color = fragmentColor;

}

在代码中通过文件路径加载顶点着色器和片段着色器,并使用一个变量存储索引。所使用的加载函数loadShaders的主要内容是读取文件和解析等操作,较为繁琐,不再贴出。

const GLchar* vertexShaderFile = "ColorArrays.vertexshader";
const GLchar* fragmentShaderFile = "ColorArrays.fragmentshader";
GLuint programID = loadShaders(vertexShaderFile, fragmentShaderFile);

在图像渲染的每次循环中,在绘图之前要先获取当前的着色器。

glUseProgram(programID);

4、VAO,VBO,EBO和colorbuffer

创建顶点数组对象(vertex array object),储存所有的顶点属性调用。

GLuint VAO;
glGenVertexArrays(1, &VAO);
//绑定至上下文
glBindVertexArray(VAO);

创建顶点缓冲对象(vertex buffer object)和索引缓冲对象(element buffer object):

GLuint VBO;
GLuint EBO;
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

创建一个通用的立方体。一个立方体有6个面,每个面由2个三角形拼成,一共需要12个三角形。静态创建图像需要将每个三角形的三个顶点的三维坐标依次写出太过繁琐,且有很多重复的数据,因此使用索引的方式动态创建。vertices数组存储立方体的八个顶点,indices数组存储12个三角形所对应的顶点的索引。

static const GLfloat vertices[] = {
    -1.0f, -1.0f, -1.0f,
    -1.0f, -1.0f, 1.0f,
    -1.0f, 1.0f, -1.0f,
    -1.0f, 1.0f, 1.0f,
    1.0f, -1.0f, -1.0f,
    1.0f, -1.0f, 1.0f,
    1.0f, 1.0f, -1.0f,
    1.0f, 1.0f, 1.0f,
};
static const GLuint indices[] = {
        0, 1, 2,
        1, 2, 3,
        1, 0, 5,
        0, 5, 4,
        5, 6, 7,
        4, 5, 6,
        2, 3, 7,
        6, 2, 7,
        1, 5, 3,
        3, 5, 7,
        0, 2, 4,
        2, 4, 6,
};

将对象绑定缓冲,并将数据载入缓冲。

//绑定到arraybuffer上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//把数组数据设置到buffer里
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_DYNAMIC_DRAW);

为顶点设置颜色属性,同样使用一个缓冲对象管理,颜色随机生成。

GLuint colorbuffer;
glGenBuffers(1, &colorbuffer);
//8个顶点,RGB三个值
static GLfloat colors[8*3];
//srand(time(NULL));
for (int v = 0; v < 8 ; v++)
{
    colors[3*v+0] = (float)rand()/RAND_MAX;
    colors[3*v+1] = (float)rand()/RAND_MAX;
    colors[3*v+2] = (float)rand()/RAND_MAX;
}
glBindBuffer(GL_ARRAY_BUFFER, colorbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

位置数据和颜色数据是图像的两个属性。将他们传给之前定义好的顶点着色器。

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,(void*)0);
glEnableVertexAttribArray(0);

glBindBuffer(GL_ARRAY_BUFFER, colorbuffer);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,0,(void*)0);
glEnableVertexAttribArray(1);

最后解除VAO的绑定。

glBindVertexArray(0);

5、MVP矩阵

每个物体原本都位于自己的局部空间内,坐标范围在-1到1之间。

模型(Model)矩阵通过平移、旋转、缩放的运算,将图形的局部坐标映射至世界坐标。

观察(View)矩阵定义了相机观察世界空间的位置和角度。

投影(Projection)矩阵确定世界空间中的坐标最终展现在屏幕前的方式。

因此,物体最终的位置由原本的位置数据加上以上三个矩阵变换而得,MVP矩阵的运算在C++代码中进行,然后在顶点着色器中计算最终的坐标数据。

MVP=projection*view*model;//C++代码
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);//顶点着色器代码

观察矩阵和投影矩阵对整个作图都是统一的。

//从着色器获得uniform变量MVP的索引
GLuint MatrixID = glGetUniformLocation(programID, "MVP");
//45°水平视野, 4:3, 展示范围远近截面从0.1到100
mat4 projection=perspective(45.0f,4.0f/3.0f,0.1f,100.0f);
//三个参数分别为相机位置,相机朝向的点的位置,相机头的方向向量
mat4 view=lookAt(vec3(4,2,0),vec3(0,0,3),vec3(0,1,0));

用之前定义的通用的立方体作为小人身体的各个部分的元件,分别设置他们的模型矩阵。一共包括身体,头,左臂,右臂,左腿,右腿,6个部件。小人的运动以身体为中心,其他五个部件以身体的位置为基础进行运动。以左手臂为例,想让小人向前走,每次渲染图像时,身体都向前平移一点儿。得到身体的模型矩阵后,计算出左肩膀位置的坐标。先缩放变换出左手臂的形状,再旋转变换左手臂在当前帧的旋转幅度,最后用平移变换将左手臂移动到之前计算出来的左肩膀位置。左手臂的旋转动作随着时间变化。

//身子每次向Z轴方向平移0.05
model_body=translate(model_body, vec3(0.0f,0.0f,0.05f));
//计算身子的MVP
MVP=projection*view*model_body;
//将MVP数据传入着色器
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
//绘制图像
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);

//左手
vec4 shd_left(1.0f,0.0f,0.0f,1.0f);
//获得身体的左肩膀位置
shd_left=model_body*shd_left;
model_hand_left=mat4(1.0f);
//手臂移动到左肩膀处
model_hand_left=translate(model_hand_left,vec3(shd_left.x+0.1,shd_left.y+0.5,shd_left.z));
//手臂随时间摆动
model_hand_left=rotate(model_hand_left,(float)sin(glfwGetTime()*2)/3,vec3(1.0f,0.0f,0.5f));
//平移手臂位置,使旋转轴位于手臂的顶端
model_hand_left=translate(model_hand_left,vec3(0.0f,-0.5f,0.0f));
//将正方体缩放成长条形的手臂形状
model_hand_left=scale(model_hand_left, vec3(0.1f,0.5f,0.1f));
//计算手臂的MVP
MVP=projection*view*model_hand_left;
//传入着色器
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
//绘制图像
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);

其他身体部位同上类似。另外给小人走过的路径绘制一个地板。

//地板
mat4 model_stay=mat4(1.0f);
model_stay=translate(model_stay, vec3(0.0f,-1.1f,5.0f));
model_stay=scale(model_stay, vec3(1.0f,0.1f,5.0f));

//...
//进入渲染循环

//地板
MVP=projection*view*model_stay;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);
//脑袋
vec4 neck(0.0f,1.0f,0.0f,1.0f);
neck=model_body*neck;
model_head=mat4(1.0f);
model_head=translate(model_head,vec3(neck.x,neck.y+0.3,neck.z));
model_head=rotate(model_head,(float)sin(glfwGetTime()*2)/5,vec3(0.0f,0.0f,1.0f));
model_head=scale(model_head, vec3(0.1f,0.1f,0.1f));
MVP=projection*view*model_head;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);
//左手
vec4 shd_left(1.0f,0.0f,0.0f,1.0f);
shd_left=model_body*shd_left;
model_hand_left=mat4(1.0f);
model_hand_left=translate(model_hand_left,vec3(shd_left.x+0.1,shd_left.y+0.5,shd_left.z));
model_hand_left=rotate(model_hand_left,(float)sin(glfwGetTime()*2)/3,vec3(1.0f,0.0f,0.5f));
model_hand_left=translate(model_hand_left,vec3(0.0f,-0.5f,0.0f));
model_hand_left=scale(model_hand_left, vec3(0.1f,0.5f,0.1f));
MVP=projection*view*model_hand_left;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);
//右手
vec4 shd_right(-1.0f,0.0f,0.0f,1.0f);
shd_right=model_body*shd_right;
model_hand_right=mat4(1.0f);
model_hand_right=translate(model_hand_right,vec3(shd_right.x-0.1,shd_right.y+0.5,shd_right.z));
model_hand_right=rotate(model_hand_right,(float)sin(glfwGetTime()*2)/3,vec3(-1.0f,0.0f,-0.5f));
model_hand_right=translate(model_hand_right,vec3(0.0f,-0.5f,0.0f));
model_hand_right=scale(model_hand_right, vec3(0.1f,0.5f,0.1f));
MVP=projection*view*model_hand_right;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);
//左脚
vec4 pp_left(0.5f,-1.0f,0.0f,1.0f);
pp_left=model_body*pp_left;
model_leg_left=mat4(1.0f);
model_leg_left=translate(model_leg_left,vec3(pp_left.x+0.05,pp_left.y,pp_left.z));
model_leg_left=rotate(model_leg_left,(float)sin(glfwGetTime()*2)/3,vec3(-1.0f,0.0f,0.0f));
model_leg_left=translate(model_leg_left,vec3(0.0f,-0.5f,0.0f));
model_leg_left=scale(model_leg_left, vec3(0.1f,0.5f,0.1f));
MVP=projection*view*model_leg_left;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);
//右脚
vec4 pp_right(-0.5f,-1.0f,0.0f,1.0f);
pp_right=model_body*pp_right;
model_leg_right=mat4(1.0f);
model_leg_right=translate(model_leg_right,vec3(pp_right.x-0.05,pp_right.y,pp_right.z));
model_leg_right=rotate(model_leg_right,(float)sin(glfwGetTime()*2)/3,vec3(1.0f,0.0f,0.0f));
model_leg_right=translate(model_leg_right,vec3(0.0f,-0.5f,0.0f));
model_leg_right=scale(model_leg_right, vec3(0.1f,0.5f,0.1f));
MVP=projection*view*model_leg_right;
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glDrawElements(GL_TRIANGLES, 12*3, GL_UNSIGNED_INT, (void*)0);

6、结果

成果图gif太大,贴不上来,可以戳进这里看。

你可能感兴趣的:(OpenGL3.3+GLFW+GLEW+GLM实现小人行走动画)