天天看點

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太大,貼不上來,可以戳進這裡看。

繼續閱讀