OpenGL教程翻译 第十一课 Concatenating Transformations

来源:互联网 发布:java工程打war包 编辑:程序博客网 时间:2024/04/28 16:05

OpenGL教程翻译 第十一课 Concatenating Transformations

原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载)

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背后为改变平移、旋转等而进行的矩阵运算的具体细节。在设置了其内部所有的参数后,提取结合了所有变换的最终矩阵。这个矩阵可直接被送到着色器中。

Code Walkthru

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

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

0 0
原创粉丝点击