准备知识
在前面的几节教程中,我们已经提到过几种变换,为物体在3D世界中的移动提供的极大的灵活性。但是我们还有很多东西需要学习(如摄像机控制和透视投影),你可以已经猜到,我们需要將这些变换组合起来。在很多情况下,你可能需要缩放一个物体使它适合3D场景,然后还要旋转使它面向正确的方向,接着把它移动到合适的位置。到现在为止我们都在练习一次只做一个变换,为了执行一系列的变换,我们要用顶点位置乘上第一个变换矩阵,然后用结果乘上第二个变换矩阵,一直乘下去直到所有的变换都应用于该顶点上。有一种简单的做法,我们可以把变换矩阵提供给shader,然后由shader来完成相乘操作。然而这种做法效率比较低,因为所有顶点都要乘上所有的变换矩阵,而这些顶点仅仅是位置不同。幸运的是,线性代数中有一个规则可以使它变得更简单。
给定一组矩阵M0…Mn 和一个向量V,则有:
Mn * Mn-1 * ... * M0 * V = (Mn* Mn-1 * ... * M0) * V
所以你可以先把矩阵的乘积算出来:
N = Mn * Mn-1 * ... * M0
则有:
Mn * Mn-1 * ... * M0 * V = N * V
这意味着我们可以先把矩阵N算出来,然后作为一个uniform类型的变量传递到shader中,在shader中把它和每一个顶点相乘,这些操作將由GPU来完成。
在把变换矩阵相乘获得N矩阵时,这些矩阵的顺序是怎样的呢?你需要记住的第一件事就是最初相乘的矩阵必须在最右边(本例中为M0),然后每个变换矩阵从右倒左依次相乘。在3D图形学中,通常先缩放对象,然后是旋转,最后才是移动。
我们来看看如果先旋转后平移会是什么情况:
接下来看看先平移后旋转的情况:
正如你所看到的,当我们先平移后旋转时,物体在世界坐标系中的位置很难把握,因为你把物体从原始点移动以后,再旋转时物体的位置也会跟着不断改变,这是我们要避免的现象。这就是为什么我们通常先做旋转变换后做平移变换。
在这个案例中我们开始处理同事存在多种变换的情况,我们必须放弃直接在渲染函数中更新变换矩阵的做法。这种方法无法扩展,容易出差,这里使用Pipeline类代替。这个类隐藏了矩阵操作的细节,使用简单的API进行平移、旋转、缩放等操作。设置了所有的参数后,你只需要简单的获取所有变换后的最终矩阵,这个矩阵可以直接用于shader中。
程序代码
清单1.主程序代码tutorial11.cpp
#include "stdafx.h"
/*
Copyright 2010 Etay Meiri
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Tutorial 11 - Concatenating transformation2
*/
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <math.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include "ogldev_util.h"
#include "ogldev_pipeline.h"
GLuint VBO;
GLuint IBO;
GLuint gWorldLocation;
const char* pVSFileName = "shader.vs";
const char* pFSFileName = "shader.fs";
static void RenderSceneCB()
{
glClear(GL_COLOR_BUFFER_BIT);
static float Scale = 0.0f;
Scale += 0.001f;
Pipeline p;
p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
p.WorldPos(sinf(Scale), 0.0f, 0.0f);
p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetWorldTrans());
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(0);
glutSwapBuffers();
}
static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderSceneCB);
glutIdleFunc(RenderSceneCB);
}
static void CreateVertexBuffer()
{
Vector3f Vertices[4];
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
Vertices[1] = Vector3f(0.0f, -1.0f, 1.0f);
Vertices[2] = Vector3f(1.0f, -1.0f, 0.0f);
Vertices[3] = Vector3f(0.0f, 1.0f, 0.0f);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}
static void CreateIndexBuffer()
{
unsigned int Indices[] = { 0, 3, 1,
1, 3, 2,
2, 3, 0,
0, 1, 2 };
glGenBuffers(1, &IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
}
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
GLuint ShaderObj = glCreateShader(ShaderType);
if (ShaderObj == 0) {
fprintf(stderr, "Error creating shader type %d
", ShaderType);
exit(1);
}
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);
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'
", ShaderType, InfoLog);
exit(1);
}
glAttachShader(ShaderProgram, ShaderObj);
}
static void CompileShaders()
{
GLuint ShaderProgram = glCreateProgram();
if (ShaderProgram == 0) {
fprintf(stderr, "Error creating shader program
");
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);
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'
", 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'
", ErrorLog);
exit(1);
}
glUseProgram(ShaderProgram);
gWorldLocation = glGetUniformLocation(ShaderProgram, "gWorld");
assert(gWorldLocation != 0xFFFFFFFF);
}
int _tmain(int argc, _TCHAR* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA);
glutInitWindowSize(1024, 768);
glutInitWindowPosition(100, 100);
glutCreateWindow("Tutorial 11");
InitializeGlutCallbacks();
// Must be done after glut is initialized!
GLenum res = glewInit();
if (res != GLEW_OK) {
fprintf(stderr, "Error: '%s'
", glewGetErrorString(res));
return 1;
}
printf("GL version: %s
", glGetString(GL_VERSION));
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
CreateVertexBuffer();
CreateIndexBuffer();
CompileShaders();
glutMainLoop();
return 0;
}
代码解读
代码中我们用到一个矩阵类Matrix4f,它提供了矩阵常用的操作 ,该类重载了乘法运算符,实现如下:
inline Matrix4f operator*(const Matrix4f& Right) const
{
Matrix4f Ret;
for (unsigned int i = 0 ; i < 4 ; i++) {
for (unsigned int j = 0 ; j < 4 ; j++) {
Ret.m[i][j] = m[i][0] * Right.m[0][j] +
m[i][1] * Right.m[1][j] +
m[i][2] * Right.m[2][j] +
m[i][3] * Right.m[3][j];
}
}
return Ret;
}
我们还用到了管道类Pipeline,声明如下:
class Pipeline
{
public:
Pipeline() { ... }
void Scale(float ScaleX, float ScaleY, float ScaleZ) { ... }
void WorldPos(float x, float y, float z) { ... }
void Rotate(float RotateX, float RotateY, float RotateZ) { ... }
const Matrix4f* GetTrans();
private:
Vector3f m_scale;
Vector3f m_worldPos;
Vector3f m_rotateInfo;
Matrix4f m_transformation;
};
Pipeline::GetTrans函数实现代码如下:
const Matrix4f* Pipeline::GetTrans()
{
Matrix4f ScaleTrans, RotateTrans, TranslationTrans;
InitScaleTransform(ScaleTrans);
InitRotateTransform(RotateTrans);
InitTranslationTransform(TranslationTrans);
m_transformation = TranslationTrans * RotateTrans * ScaleTrans;
return &m_transformation;
}
这个函数中初始化三个独立的变换矩阵,然后返回三个矩阵的乘积作为最终的变换矩阵。相乘的顺序是通过硬编码写死的。需要注意的是最终变换矩阵是该类的成员属性,你可以通过检查一个脏数据标志位来优化该方法,返回一个存储的矩阵以防止函数最后一次调用时配置上没发生任何变化。
主程序中代码:
Pipeline p;
p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
p.WorldPos(sinf(Scale), 0.0f, 0.0f);
p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetTrans());
这里是主程序中和上一节不同的地方,我们定义一个Pipeline 对象,通过该对象的成员函数来完成缩放、平移、旋转变换。
运行效果
可以看到四面体在屏幕上同时移动、旋转、变大变小。