×
关于

现代 OpenGL 教程

第一课 创建一个窗口第二课 绘制一个点第三课 第一个三角形第四课 着色器基础第五课 一致变量第六课 平移变换第七课 旋转变换第八课 缩放变换第九课 插值第十课 索引绘制第11课 矩阵连乘第12课 透视投影变换第13课 相机空间第14课 相机控制(一)第15课 相机控制(二)第16课 基本的纹理贴图第17课 环境光第18课 漫反射光第19课 镜面反射光第20课 点光源第21课 聚光灯第22课 使用Assimp库导入模型第23课 Shadow Mapping 1第24课 Shadow Mapping 2第25课 天空盒第26课 法线纹理第27课 广告牌和几何着色器第28课 创建粒子系统第29课 3D拾取第30课 细分着色器基础第31课 基于PN三角形细分着色第32课 顶点数组对象第33课 实例渲染第34课 GLFX OpenGL特效库第35课 延迟渲染(一)第36课 延迟渲染(二)第37课 延迟渲染(三)第38课 Assimp导入骨骼动画第39课 轮廓检测第40课 模板阴影体第41课 运动模糊第42课 基于 PCF 的阴影优化第43课 点光源多通道阴影映射第44课 GLFW第45课 屏幕空间的环境光遮挡第46课 SSAO与深度重构

第11课 矩阵连乘


Background

在前几章中我们学习了一些变换,通过它们我们能将3D世界中的物体灵活的变换到任意位置。之后我们还要学习两个变换(相机控制和透视投影),但是正如你可能已经猜到的,我们需要一个变换的组合。在很多情况下,你需要缩放物体以适应你的 3D 世界的尺寸,将其旋转到需要的方向,平移到某处等等。直到现在,我们每次都只能使用一个单一的变换。为了实现上述一系列的变换,我们需要用顶点坐标与第一个变换矩阵相乘,然后将前面的结果与下一个的变换矩阵相乘。如此持续到所有的变换矩阵都应用到顶点上面。要实现这个效果,最简单的办法就是向着色器提供所有的变换矩阵并依次让他们做相乘运算。然而,这种方法非常低效,因为对于顶点来说所有的变换矩阵都是一样的,而改变的只是顶点的位置而已。幸运的是,线性代数提供了一套规则使得我们的处理变得简单。这个规则告诉我们,给定一组矩阵 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 计算出来,然后将其作为一致变量传递到着色器和每个顶点相乘。这就要求 GPU 对每个顶点只进行一次矩阵或者向量的相乘。

当我们生成 N 时你是怎么安排矩阵相乘的顺序的?需要注意的是向量会首先和最右边的矩阵相乘(本例中是M0)。然后从右向左,向量依次被每个矩阵变换。在3D图形中,你常常会希望先缩放物体然后旋转、平移,然后应用相机变换,最后经过透视投影变换到2D屏幕上。让我们看看如果你先旋转再平移这会发生什么:

现在我们再看看先平移再旋转发生了什么:

正如你看到的,在世界坐标系中,当你先平移物体后很难设置它的位置,因为如果你将其从原点移开然后再旋转,它会围绕着原点旋转,这实际上相当于你再次平移了这个物体。一般情况下,你都不会想出现这样的二次平移,所以我们可以通过先旋转然后平移的方法,断开这两种操作的依赖关系。这就是为什么建模应围绕原点并且越对称越好。这样当你缩放或者旋转物体时都没有副作用,而且被旋转或缩放的物体仍然会保持对称。

既然我们已经开始处理多个矩阵的变换,那么我们最好扔掉直接在渲染函数中更新矩阵的习惯,这个方法效果不怎么好还容易出错。取而代之我们引入了管线类。在这个类中我们使用一个简单的 API 来封装平移、旋转以及缩放矩阵的运算的具体细节。在设置了其内部所有的参数后,我们可以获得多个矩阵运算的最后结果矩阵。这个矩阵可直接被送到着色器中。

代码

 #defineToRadian(x) ((x) * M_PI / 180.0f)
 #define ToDegree(x) ((x) *180.0f / M_PI)

在这章我们开始使用角度的弧度值。碰巧的是,标准C语言库里面也使用弧度作为参数。上面的宏命令将角度和弧度互换。

inline Matrix4foperator*(const Matrix4f& Right) const
{
    Matrix4fRet;
    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];
      }
    }
    returnRet;
}

重载*号运算符用于矩阵相乘,正如你看到的,在矩阵相乘运算中,我们使用左边矩阵每一行与右边矩阵的相应列进行点成操作。这个操作在管线类的实现上至关重要。

class Pipeline
{
       public:
              Pipeline()
              { ...  }
              void Scale(floatScaleX, float ScaleY, float ScaleZ)
              { ... }
              void WorldPos(float x,float y, float z)
              { ... }
              void Rotate(floatRotateX, float RotateY, float RotateZ)
              { ... }
              const Matrix4f*GetTrans();
       private:
              Vector3f m_scale;
              Vector3f m_worldPos;
              Vector3f m_rotateInfo;
              Matrix4fm_transformation;
};

管道类抽象了对对象进行矩阵变换的所有细节。这里有三个私有成员向量来储存对象的缩放、世界坐标位置和旋转轴。另外还有API来设置他们,以及一个用来得到代表这些变换的最终矩阵的函数。

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());

这些是渲染函数的变化。我们指定一个管线类对象,配置它并且将结果矩阵传递到着色器。我们也可以修改这些参数,在最后的图像中来看看它们的效果。


分类导航

关注微信下载离线手册

bootwiki移动版 bootwiki
(群号:472910771)