《3D绘图程序设计》彭国伦

本书分3个部分。第1~10章介绍传统的固定绘图流程和基本3D绘图概念,包括坐标转换、动画与交互、打光、贴图、混合与纹理、动态贴图、Stencil Buffer和特效处理等内容。第11~18章为比较高级的Shader程序编写,包括HLSL和GLSL的使用、Shader特效和调试Debug等内容。第19~20章是补充教学,介绍绘图引擎、Xbox360、PS3、GPGPU和线性代数等基础知识。

并不是每位读者学习3D绘图都是为了做出炫丽的游戏画面,很多读者学习3D绘图只是为了把数据进行图形化显示。

本书网页:
http://www.cmlab.csie.ntu.edu.tw/~perng/3D
DirectX SDK下载地址: 
http: //www.microsoft.com/ downloads
OpenGL指令说明:
http://www.opengl.org/sdk/docs/man/
NVIDIA FX Composer 和CG的下载地址:
http://developer.nvidia.com
Render Monkey 的下载地址:
http://ati.amd.com/developer/rendermonkey/downloads.html
太阳系行星图主要来自以下两个网页:
http: //www.nasa.gov/
http: //planetpixelemporium.com/
环境贴图Cubemap 和高动态范围HDR图片来自:
http://www.debevec.org/
模型和其他贴图主要来自:
http://www.turbosquid.com/

第一章 计算机绘图简介

现实世界中存在许多不规则形状的物体,不是每样东西都长得像箱子,刚好用8个顶点、6个面就可以搞定。使用面积愈小、数量愈多的三角形,来逼近像人体之类的不规则物体,可以得到更高的质量,不过在绘图时也会花费更多的时间来处理这些三角形。一般的绘图芯片GPU常会使用每秒能处理几个三角形为单位来说明它的执行速度。

单纯以每秒能处理几个三角形为单位,其实不能精确地说明绘图芯片的能力,因为三角形可能有大有小。目前,一般还会在规格上说明芯片每秒能处理几个顶点、每秒能够填充多少个像素(称为fillrate)、芯片读写内存的速度等。

纯碎使用多边形时,画面上的最终成像结果只有物体的形状而已。想要看到更逼真的物体,可以再使用贴图。简单地说,贴图就是一张有图案的贴纸,把贴纸贴在三角形上。

物体远近如果存在遮挡,则需要用到zbuffer,即深度缓存。如果先绘制近处的物体,当绘制远处物体体便会通过zbuffer来决定远处的物体哪些要画,哪些不要画。

由于游戏要跟玩家交互,画面必须是实时产生出来的。一般来说,游戏程序最起码都要配置两块画面内存,一块用来放已经画好的图;另一块则交给程序去准备下一个画面,这就是所谓的double buffer。正在显示的画面称为前景front buffer,准备中的画面称为背景back buffer。

GPU也就是显示芯片。大概在上世纪90年代初期,随着Windows操作系统的流行,市面上出现2D绘图加速卡。这些产品和现在的3D绘图芯片有一段很大的差距,它们的功能大致上就是让画线、图块搬动等的指令变快。那个年代3D加速只存在于工作站等级的计算机上。

上世纪90年代末期,个人计算机也开始慢慢流行3D加速卡。那时候的3D加速部分还没有提供百分百硬件加速,多边形的坐标转换仍然是要靠CPU实现。不过,它们对最耗费系统资源的隐藏面消除Z Buffer Test和使用贴图两大部分提供硬件加速。

大约到2000年的时候,市面上开始出现百分百3D硬件加速产品。这类的绘图芯片可以自动计算多边形的坐标转换,也可以计算多边形顶点的光照,大幅减少了3D绘图对于CPU的依赖程度。它们也开始通过OpenGL的扩展功能来提供一些Shader的可程序化功能,不过这个时候的Shader还没有称为工业标准,所以并不是标准化的Shader语言,因此它不太好用。

到了2001年,配合新版的DirectX8.0,市面上有了第一组可程序化的绘图芯片NVIDIA Geforce 3。程序员可以对这些芯片编写程序来自定义顶点的坐标转换和光照的过程,也就是Vertex Shader或叫做Vertex Program。也可以写程序来控制画面上每个像素的着色计算,称为Pixel Shader或叫Fragment Program。

第一个版本的Shader在使用上有很大的限制,例如程序代码不能太长,尤其是Pixel Shader,不支持循环Loop和If-Else程序判断。另外,Pixel Shader计算的精确度很低,常常不能用直觉的算式来编写程序。接下来的几款硬件,在Shader编写上开始允许比较高的自由度,也开始在某个程度上提供循环Loop和If-Else的使用。新版的还支持新的功能,称为Geometry Shader,可以让程序员控制几何图形的产生过程。

本书的第2-10章会介绍固定流程Fixed Function Pipeline的绘图,也就是2001年之前的GPU所支持的功能;第11-17章会介绍可程序化流程Shader,也就是在2001年之后GPU所新增的功能。

第二章 坐标转换 

第一个程序:在窗口正中央画一个小白点。

int main(void)
{
    GLFWwindow* window;

    if (!glfwInit())
        return -1;
    
    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    if (glewInit() != GLEW_OK)
        std::cout << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION)<<std::endl;

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        RenderFrameOpenGL();

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }
}
// `使用OpenGL`
void RenderFrameOpenGL(void)
{
    // `清除画面`
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // `画一个点`
    glBegin(GL_POINTS);
    glVertex3fv(vertex);
    glEnd();

    // `把背景backbuffer的画面显示出来`
    //SwapBuffers(g_hDC);;
}

第一步,创建一个窗口。

第二步,将该窗口设为默认窗口。

第三步,进入循环绘制。

第四步,将绘图的buffer传至交换区。

本书的坐标系都使用右手坐标系,即Z轴朝外为正值。x从左向右为-1到1。y从下向上为-1到1。

大部分情况下,不会使用上面第一个程序范例来进行3D绘图,不是因为只画了一个小白点很不专业,而是在于设置3D坐标点的方法。范例程序直接使用屏幕坐标系来定位,没有使用任何坐标转换,接下来进入非常重要的3D坐标转换。

简单地说,3D绘图的图像投影方法分成两种:正交视图(Orthogonal View)和透视图(Perspective View)。正交视图镜头下的3D对象,无论远近看起来都一样大。透视图的镜头下,远的物体看起来会变小,比较接近现实世界的情况。

范例程序chap2/orthogonal_view画出了一个金字塔的8条边,因为它是使用正交视图的关系,再加上是从金字塔顶端向下看,所以感觉不到任何立体感。g_vertices数组里面存放了16个3D顶点的数据,刚好每两个顶点为一组,总共形成8条边。在此直接使用未经转换过、落在世界坐标系上的3D坐标来记录这16个顶点,而不是像范例程序chap02/point那样直接使用屏幕坐标系来定位。

《3D绘图程序设计》彭国伦

第二个程序:orthogonal_view

//金字塔                                         
// `金字塔形的8条边线`
Vector4 g_vertices[] =
{
    Vector4(-1.0f, 1.0f,-1.0f),
    Vector4(-1.0f,-1.0f,-1.0f),

    Vector4(-1.0f,-1.0f,-1.0f),
    Vector4(1.0f,-1.0f,-1.0f),

    Vector4(1.0f,-1.0f,-1.0f),
    Vector4(1.0f, 1.0f,-1.0f),

    Vector4(1.0f, 1.0f,-1.0f),
    Vector4(-1.0f, 1.0f,-1.0f),

    Vector4(0.0f, 0.0f, 1.0f),
    Vector4(-1.0f, 1.0f,-1.0f),

    Vector4(0.0f, 0.0f, 1.0f),
    Vector4(-1.0f,-1.0f,-1.0f),

    Vector4(0.0f, 0.0f, 1.0f),
    Vector4(1.0f,-1.0f,-1.0f),

    Vector4(0.0f, 0.0f, 1.0f),
    Vector4(1.0f, 1.0f,-1.0f),
};
//镜头位置
Vector4 g_eye(0.0f, 0.0f, 5.0f);
//镜头对准的点
Vector4 g_lookat(0.0f, 0.0f, 0.0f);
//镜头正上方的方向
Vector4 g_up(0.0f, 1.0f, 0.0f);

int main(void)
{
  //....
}

//使用OpenGL
void RenderFrameOpenGL(void)
{
    //清除画面`
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //计算出一个可以转换到镜头坐标系的矩阵
    Matrix4x4 view_matrix = GutMatrixLookAtRH(g_eye, g_lookat, g_up);
    //计算出一个使用正交投影的矩阵`
    Matrix4x4 orthogonal_matrix = GutMatrixOrthoRH_OpenGL(5.0f, 5.0f, 0.1f, 100.0f);
    //把这两个矩阵相乘`
    Matrix4x4 view_orthogonal_matrix = view_matrix * orthogonal_matrix;
    //把空间中的坐标点转换到屏幕坐标系上`
    Vector4 vertices[16];
    for (int i = 0; i<16; i++)
    {
        vertices[i] = g_vertices[i] * view_orthogonal_matrix;
        vertices[i] /= vertices[i].GetW();
    }

    // 画出金字塔的8条边线`
    glBegin(GL_LINES);
    for (int l = 0; l<8; l++)
    {
        glVertex3fv((float *)&vertices[l * 2]);
        glVertex3fv((float *)&vertices[l * 2 + 1]);
    }
    glEnd();

}

在此不再使用float类型数组,而是使用Vector4的对象来记录顶点3D位置。Vector4对象中定义了很多3D绘图中经常会用到的线性代数计算函数,读者可以自行加载Vector4_SSE.h和Vector4_SSE.cpp来看它们的实现方法。为了追求效率,Vector4使用了Intel SSE指令集。没有使用Intel SSE来实现的版本Vector4_Reference.h和Vector4_Reference.cpp,这个版本很容易理解。SSE版本和标准C++版本所做的是完全相同的事情,唯一的区别是前者使用SSE汇编语言指令集来实现优化。提供SSE版本的目的是为了方便后面的章节介绍Shader程序设计时作对比。

范例中还用到了2个矩阵,同样的,提供了一个Matrix4x4的C++对象来处理矩阵计算,Matrix4x4_SSE是使用Intel SSE指令集加速的版本,读者不需要去了解它,可以去参考使用标准C++版本的Matrix4x4_Reference.第一个矩阵是使用GutMatrixLookAtRH函数所生成的镜头坐标系转换矩阵。3D向量和这个矩阵相乘后,会被转换成顶点相对于镜头的位置,来看看这个矩阵是如何计算出来的。第20章会详细说明这些转换矩阵的生成方法,有兴趣的读者可以先翻过去看看。

第2个矩阵是做正交投影用的矩阵,它功能单一,只会把转换到镜头坐标系上的3D向量值缩放,让应该出现在画面可视范围内的顶点落在屏幕上。在这里调用GutMatrixOrthoRH_DirectX时传入w=5以及h=5,等于是把屏幕坐标系的可视范围放大。但是,前面提过屏幕坐标系的范围都是固定的,所以把可视范围加大的方法是先把镜头坐标系上的向量X和Y的值都乘以2/5缩小后,再转换到屏幕坐标系上。

Z值的转换比较复杂,不只是简单地乘上某个数值来缩小,读者可以试着自行代入数值和投影矩阵相乘,当3D向量中的Z=Znear时,转换后的Z是0。当Z=Zfar时,转换后的Z值是1。换句话说,只有相对于镜头Z轴的距离在Znear到Zfar之间的向量,才会落在可视范围内。

有了这两个矩阵,就可以得到正交投影的结果。3D顶点先乘以view_matrix矩阵转换到镜头坐标系上,再把转换后的向量乘以orthogonal_matrix矩阵,就可以得到使用正交投影法转换到屏幕坐标系上的结果。根据线性代数的原理,我们也可以把view_matrix和orthogonal_matrix先两两相乘,得到另一个view_orthogonal_matrix矩阵,再把3D顶点直接和新的view_orthogonal_matrix矩阵相乘,就可以把它转换到屏幕坐标系上。(P25)

调用DrawPrimitiveUP时传入D3DPT_LINELIST,指定要画的几何图形是线段。一个金字塔有8条边,每条边由两个顶点组成,总共就刚好是16个点。DrawPrimitiveUP的最后一个参数设置每个顶点数据的大小,在这里总共会传递16个顶点,所以要把大小设置好,GPU才会正确地读到所有顶点数据。

接下来是透视图投影。正交视图在制作工具时很有用,它可以精确地比较两个对象的大小,也可以精确的定位,其缺点是看起来比较缺乏立体感。这个程序跟上一个范例程序几乎完全相同,区别在于镜头位置不太一样。另外就是获得投影矩阵的方法不同。

《3D绘图程序设计》彭国伦

《3D绘图程序设计》彭国伦

第三个程序:chap02/perspective_view/perspective_view.cpp

//使用OpenGL来绘图
void RenderFrameOpenGL(void)
{
  //清除画面
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
//计算出一个可以转换到镜头坐标系的矩阵
Matrix4x4 view_matrix = GutMatrixLookAtRH(g_eye, g_lookat, g_up);
//计算出一个透视投影的矩阵
Matrix4x4 perspective_matrix = GutMatrixPerspectiveRH_OpenGL(90.0f,1.0f,1.0f,100.0f);
//把这两个矩阵相乘
Matrix4x4 view_perspective_matrix = view_matrix*perspective_matrix;
//把空间中的坐标点转换到屏幕坐标系上
Vector4 vertices[16];
for(int i=0;i<16;i++)
{
vertices[i] = g_vertices[i]*view_perspective_matrix;
vertices[i] /= vertices[i].GetW();
}
//画出金字塔的8条边
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(4, GL_FLOAT, sizeof(Vector4), vertices);
glDrawArrays(GL_LINES, 0, 16);
//把背景backbuffer的画面显示出来
GutSwapBuffersOpenGL();
}

OpenGL版本调用GutMatrixPerspectiveRH_OpenGL来计算投影矩阵。同样,OpenGL矩阵的特点在于它的屏幕坐标系Z轴的可视范围在-1~1之间。这个范例还有一个小小的改变,画线的指令不再去调用glBegin(GL_LINES)和glEnd(),而是改用另外一个比较有效率的方法。使用glVertexPointer设置3D坐标点的内存位置及数据类型,再使用glDrawArrays一口气把vertices数组中的顶点全部画出来。这个方法使用的CPU指令少了很多,上一节使用循环调用glVertex3fv高达16次来画出8条线。

以上是自己设计的向量和矩阵的乘法。但是现在很多绘图芯片都提供硬件加速的坐标转换功能,只要把转换矩阵设置好,GPU会自动处理坐标转换。就算GPU不支持坐标转换的功能,还是可以自动调用CPU来处理坐标转换,不需要在程序中自行计算。范例程序chap02/perspective_view2的运行结果跟上一节的范例的运行结果看起来一模一样,区别在于前者不再自行实现向量跟矩阵的相乘。

 第四个程序:chap02/perspective_view2/perspective_view2.cpp

//使用OpenGL来绘图
void RenderFrameOpenGL(void)
{
  //清除画面
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  //计算出一个可以转换到镜头坐标系的矩阵
  Matrix4x4 view_matrix = GutMatrixLookAtRH(g_eye, g_lookat, g_up);
  //设置镜头转换矩阵
  glMatrixMode(GL_MODELVIEW);
  glLoadMatrixf((float*) &view_matrix);
  //计算出一个透视投影的透视矩阵
  Matrix4x4 perspective_matrix = GutMatrixPerspectiveRH_OpenGL(90.0f, 1.0f, 1.0f, 100.0f);
  //设置视角转换矩阵
  glMatrixMode(GL_PROJECTION);
  glLoadMatrixf((float*) &perspective_matrix);
  //画出金字塔的8条边
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(4, GL_FLOAT, sizeof(Vector4), g_vertices);
  glDrawArrays(GL_LINES,0,16);
  //把背景backbuffer的画面显示出来
  GutSwapBuffersOpenGL();
}

OpenGL其实提供了函数用来生成坐标转换矩阵,不需要写代码来做这件事。下一个范例程序将会演示如何使用这些函数。这里比较特别的一点是,代码中并没有刻意去存储和设置这两个矩阵,gluLookAt和gluPerspective会自动把计算出来的矩阵,乘以目前正在操作的矩阵,并使用计算结果。这也是为什么在调用gluLookAt和gluPerspective之前,都会先调用glLoadIdentity把矩阵设置成单位矩阵,接着才乘以新的矩阵。OpenGL使用设置状态的方法来控制硬件,设置好某个参数后,这个参数在没有被其他人改变之前,会永远保持不变的数值。

第五个程序:chap02/perspective_view3/perspective_view3.cpp

// `使用OpenGL来绘图`
void RenderFrameOpenGL(void)
{
    // `清除画面`
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // `计算并设置镜头转换矩阵`
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(
        g_eye[0], g_eye[1], g_eye[2],
        g_lookat[0], g_lookat[1], g_lookat[2],
        g_up[0], g_up[1], g_up[2]);
    // `计算并设置视角转换矩阵`
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(90.0f, 1.0f, 1.0f, 100.0f);

    // `画出金字塔的8条边线`
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(4, GL_FLOAT, sizeof(Vector4), g_vertices);
    glDrawArrays(GL_LINES, 0, 16);

    // `把背景backbuffer的画面显示出来`
    GutSwapBuffersOpenGL();
}

本书的大部分范例都会自行创建坐标转换矩阵,而不使用OpenGL提供的函数。这样做的主要原因是这种方法比较容易随时掌握所有的转换矩阵,在写Shader时比较方便;而且除了投影矩阵之外,其他矩阵的计算方法和绘图平台没有关系,自行创建转换矩阵在跨平台时反而比较简单。使用OpenGL提供的函数并不是不好,只是除了方便之外也没有什么明显的好处。

Fixed Function Pipeline若依照字面翻译,会是一个很奇怪的词“固定流程管线”。笔者不太喜欢这种翻译方法,在此把它简单地称为固定流程绘图。Fixed Function Pipeline简单地说,是指在绘图芯片还没有进化到Shader之前的年代,GPU的所有功能都是预先设置好的,固定地写在硬件线路中,它只能使用固定的流程来进行坐标转换和光照计算、使用预先定义好的几种方法来存取贴图;程序员所能做的控制是设置转换矩阵、放入光源数据、选择要使用哪几张贴图,然后再使用模型来绘图。前面几节所看到的范例程序,都是使用Fixed Function Pipeline固定流程的方法来绘图。

Fixed Function Pipeline发展到后来出现的问题是,因为所有的功能都是预先定义好的,程序员只能通过传入不同参数,打开或关闭不同开关的方法来控制硬件。这些预先定义好的功能永远不能满足程序员的需求。硬件每增加一项功能,OpenGL都要相应地新增相应的参数或函数来控制硬件。最好的解决办法是把绘图芯片GPU转变成可程序化的,让程序员可以自行编写程序来控制绘图的流程,就像编写C++程序给CPU执行一样,这就是Shader的功能。

我们也可以把Fixed Function Pipeline称为不可程序化流程,把Shader称为可程序化流程。这也不算是很好的翻译,不过比较容易从字面上来了解它们的意义。想要完全了解Shader的使用,不能跳过Fixed Function Pipeline的部分。因为启动可程序化流程后,有很多控制绘图芯片的参数和方法还是沿用了Fixed Function Pipeline的指令和规则,例如混合Blend、Stencil Buffer和Z Buffer的控制等。

Direct3D9和OpenGL可以选择在使用Fixed Function Pipeline和Shader之间切换,但Direct3D10只支持Shader。笔者认为,这是为了简化硬件,新的芯片只要专注于Shader,而不需要浪费硬件线路去支持固定流程。事实上,目前有些新的GPU在处理Direct3D9和OpenGL的Fixed Function Pipeline时,是通过驱动程序或者硬件本身生成Shader来绘图。GPU实际上只支持可程序化流程。

本书第11章才开始正式进入Shader的教学。不过由于Direct3D10只支持Shader的绘图,所以前面2-10章还是难免会出现Shader的范例程序,这些Shader都只是把Fixed Function Pipeline的功能重做一次,让Direct3D10的范例程序能够运行。

下面的范例chap02/perspective_view_shader使用Shader来画金字塔,运行结果和前面两个范例完全相同,唯一的区别就是这里使用了Shader。

第六个程序:chap02/perspective_view_shader/render_dx9.cpp

#include "Gut.h"
#include "render_data.h"

static LPDIRECT3DVERTEXSHADER9 g_pVertexShaderDX9 = NULL;
static LPDIRECT3DPIXELSHADER9  g_pPixelShaderDX9 = NULL;
static LPDIRECT3DVERTEXDECLARATION9 g_pVertexShaderDecl = NULL;

bool InitResourceDX9(void)
{
    // `载入Vertex Shader`
    g_pVertexShaderDX9 = GutLoadVertexShaderDX9_HLSL(
        "../../shaders/vertex_transform.shader", "VS", "vs_1_1");
    if ( g_pVertexShaderDX9==NULL )
        return false;
    // `载入Pixel Shader`
    g_pPixelShaderDX9 = GutLoadPixelShaderDX9_HLSL(
        "../../shaders/vertex_transform.shader", "PS", "ps_2_0");
    if ( g_pPixelShaderDX9==NULL )
        return false;

    // `镜头坐标系转换矩阵`
    Matrix4x4 view_matrix = 
        GutMatrixLookAtRH(g_eye, g_lookat, g_up);
    // `投影转换矩阵`
    Matrix4x4 projection_matrix = 
        GutMatrixPerspectiveRH_DirectX(90.0f, 1.0f, 1.0f, 100.0f);
    // `把前两个矩阵相乘`
    Matrix4x4 view_projection_matrix = 
        view_matrix * projection_matrix;
    
    LPDIRECT3DDEVICE9 device = GutGetGraphicsDeviceDX9();
    // `设置视角转换矩阵`
    device->SetVertexShaderConstantF(0, (float *)&view_projection_matrix, 4);

    return true;
}

bool ReleaseResourceDX9(void)
{
    if ( g_pVertexShaderDX9 )
    {
        g_pVertexShaderDX9->Release();
        g_pVertexShaderDX9 = NULL;
    }

    if ( g_pPixelShaderDX9 )
    {
        g_pPixelShaderDX9->Release();
        g_pPixelShaderDX9 = NULL;
    }

    return true;
}

// `使用DirectX9来绘图`
void RenderFrameDX9(void)
{
    LPDIRECT3DDEVICE9 device = GutGetGraphicsDeviceDX9();
    device->Clear(
        0, NULL, // `清除整个画面`
        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, // `清除颜色和Z Buffer`
        D3DCOLOR_ARGB(0, 0, 0, 0), // `设置要把颜色清成黑色`
        1.0f, // `设置要把Z值清为1, 也就是离镜头最远.`
        0 // `设置要把Stencil buffer清为0, 在这是否设置没有区别.`
    );
    
    // `开始下绘图指令`
    device->BeginScene(); 
    // `设置数据格式`
    device->SetFVF(D3DFVF_XYZW); 
    // `设置Vertex/Pixel Shader`
    device->SetVertexShader(g_pVertexShaderDX9);
    device->SetPixelShader(g_pPixelShaderDX9);
    // `画出金字塔的8条边线`
    device->DrawPrimitiveUP(D3DPT_LINELIST, 8, g_vertices, sizeof(Vector4)); 
    // `声明所有的绘图指令都下完了`
    device->EndScene(); 
    
    // `把背景backbuffer的画面显示出来`
    device->Present( NULL, NULL, NULL, NULL );
}

选用Direct3D9来绘图时,主程序中的init_resource函数指针会指向InitResouceDX9函数,这个函数会调用GutLoadVertexShaderDX9_HLSL和GutLoadPixelShaderDX9_HLSL来载入vertex_transform.shader中的Vertex Shader和Pixel Shader。暂时先不去看加载Shader的代码,只需要知道它们可以加载Vertex Shader和Pixel Shader就行了,有兴趣的读者可以自行把这两个函数从glib/GutDX9.cpp中找出来看,第12章会详细说明它们的实现方法。

vertex_transform.shader使用Direct3D9 HLSL语言来实现Shader,它的功能很简单,Vertex Shader部分和前面使用CPU来实现坐标转换的代码一样,只是把顶点位置和转换矩阵相乘。Pixel Shader部分更简单,它永远填充白色。

HLSL和CG Shader的代码看起来很类似C++,不过有几点和C++不同。如它们都没有固定的起点,main函数并不固定,所以在加载Shader时要特别指定哪个函数是程序入口点。还有,在定义Vertex Shader传入和返回值的结构类型structure时,需要对struct的每个成员指定它所代表的意义。例如,VS_INPUT中的Position成员被对应到POSITION字段,代表Position这个成员是对应到顶点数据字段的3D坐标位置。VS_OUTPUT中的POSITION字段则指定它会存放转换后的屏幕坐标位置。Pixel Shader的主程序,最后把它的返回值指定放在COLOR字段,代表函数PS的返回值是用来更新画面颜色。Vertex Shader和Pixel Shader每运行一次计算,都是以一个向量(x,y,z,w)和4个数值为单位同时来做计算。

Vertex Shader获得数据的方法有3种,最简单的方法是从顶点数据区Vertex Buffer来获得;调用类似DrawPrimitiveUP的函数时所传入的顶点数据,就是在指定这组数据。第3种方法是从常量寄存器Constant Register种获得数据。

Pixel Shader也有3种方法可以获得数据,一种是从Vertex Shader种传入数值,另一种同样是通过常量寄存器。通过Vertex Shader传入的数值,会根据像素落在三角形中的位置来内插,获得介于3个顶点间的数值。

第七个程序:shaders/vertex_transform.shader

// `設定頂點的資料格式`
struct VS_INPUT
{
    float4 Position : POSITION;
};

// `設定Vertex Shader輸出的資料格式`
struct VS_OUTPUT
{
    float4 Position : POSITION;
};

// `轉換矩陣`
uniform row_major float4x4 viewproj_matrix : register(c0);
uniform float4x4 viewproj_matrix_t : register(c0);

// Vertex Shader
VS_OUTPUT VS(VS_INPUT In)
{
    VS_OUTPUT Out;
    // `座標轉換`
    Out.Position = mul(In.Position, viewproj_matrix);
    // `不把矩陣視為 row major 時, 乘法要反過來做.`
    // Out.Position = mul( viewproj_matrix_t, In.Position );

    return Out;
}

// Pixel Shader
float4 PS(VS_OUTPUT In) : COLOR
{
    // `傳回白色, 永遠畫一個白點.`
    return float4(1,1,1,1);
}

绘图芯片都是用设置状态的方法来控制,这点不论是使用Direct3D还是OpenGL都差不多。设置好要使用某个Shader后,没有改变前,芯片会固定地运行同一个Shader。同样的,在某个Constant Register常量寄存器中放入了特定数值后,只要代码没有改变它,这个数值会永远存在。Render_dx9.cpp只在InitResourceDX9函数中计算转换矩阵,并且把矩阵存放在Vertex Shader的第0~3个constant register中,然后就不再改变它。在绘图循环中每次调用RenderFrameDX9时,Vertex Shader会读取到相同的转换矩阵。(device->SetVertexShaderConstantF

[<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

在3D绘图中,4*4矩阵可以用来说明一个坐标系。当一个四维向量和一个4*4矩阵相乘后,计算所得到的四维向量就是原始向量被转换过的新位置。可以把4*4矩阵切成两个部分来看,左上角的3*3部分用来定义坐标系的轴向,也就是法线,换个说法,也就是旋转。左下角的1*3部分是位移。所有的坐标转换都可以想象成先经过左上角的3*3部分来旋转后,再通过左下角的1*3部分来位移。

《3D绘图程序设计》彭国伦

移动向量

这里所指的移动包含旋转和位移。(向量和坐标的区别?)最简单的转换矩阵是单位矩阵,每个向量和它相乘后结果都不会改变。在Direct3D和OpenGL中模拟2D显示时,有时候会直接代入单位矩阵,这样可以方便程序员在屏幕上定位。

稍微复杂的转换矩阵是位移矩阵,它左上角的3*3部分不会对向量做任何旋转,只有左下角的1*3部分对向量位移。在和4*4矩阵相乘时,在这个三维向量后面补一个1,让它变成四维向量。放入1而不放入其他数值有它的意义所在,当四维向量的最后一个数值是1时,刚好可以通过向量和矩阵相乘的公式,让矩阵的左下角1*3部分对向量生成位移效果。下面的式子很清楚地说明了这个动作。

《3D绘图程序设计》彭国伦

通常,有旋转功能的矩阵看起来会比较复杂。这里使用看起来比较简单的矩阵来演示,它用来对X轴逆时针旋转90度,这里把位移的部分去掉以对它进行简化。

公式:对X轴旋转的矩阵

《3D绘图程序设计》彭国伦

把对X轴旋转后的Y和Z的新轴向找出来,就是旋转矩阵的内容。由于是对X轴旋转,所以X轴方向固定是(1,0,0)。在右手坐标系中,从正X轴向原点看过去,Y、Z轴的方向如图所示。

公式:对Y轴旋转的矩阵

《3D绘图程序设计》彭国伦

把对Y轴旋转后的X和Z的新轴向找出来,就是旋转矩阵的内容。由于是对Y轴旋转,所以Y轴方向固定是(0,1,0)。在右手坐标系中,从正Y轴向原点看过去,X、Z轴的方向如图所示。

公式:对Z轴旋转的矩阵

《3D绘图程序设计》彭国伦

改变参考点

上面所看到的例子是通过矩阵把向量V从它原来的位置移动到新的位置上。有时候不希望移动任何向量,而只是希望改变观察点,并找出向量相对于新观察点的位置,如图20.6所示。

《3D绘图程序设计》彭国伦

 改用相对位置所获得的数据有时会比较有用。

在没有旋转的情况下,计算相对位置是一件很容易的事情。带入旋转后,概念就会比较复杂。不过从线性代数的观点来看,这两种情形是一样的。不管是否有旋转都可以用相同的方法来解决。先直接来看下面的式子,在移动一个向量时,它事实上就是假设向量中所记录的是它在矩阵所代表的坐标系上的相对位置,所以向量会随着这个坐标系移动。

《3D绘图程序设计》彭国伦

对向量做移动时,Plocal是已知的,所要计算的是Pworld;而在求相对位置时,情形相反。我们已经预先知道向量的最终位置,所要求的是它对某个坐标系为参考点的相对位置,所以这时Pworld是已知的,未知数变成Plocal。上面的等式仍然成立,可以对在左右两边同时乘上M-1矩阵。

《3D绘图程序设计》彭国伦

由上面的式子可以发现,把向量乘以某个坐标系的逆矩阵,就会得到向量在这个坐标系上的相对位置。3D绘图中经常要计算对象对镜头的相对位置,把对象在世界坐标系上的位置乘以镜头矩阵的逆矩阵,就会得到对象相对于镜头的位置。要做阴影效果时,还需要计算对象相对于光源的位置,方法同前,唯一的区别在于所要代入的矩阵不同。

创建镜头矩阵的方法很简单,根据前面介绍旋转和位置矩阵的方法来做就可以了。先找出镜头法线的3个轴向,把这三个轴向放入矩阵左上角的3*3部分,再把镜头的位置放入矩阵左下角的1*3部分,所得到的就是镜头的移动矩阵。接下来求出这个矩阵的逆矩阵,就可以通过这个逆矩阵获得3D顶点到镜头的相对位置。

计算逆矩阵时,可以用前面介绍过的求解16个等式的方法来做。但是,由于镜头矩阵通常只具有旋转或位移,这时可以使用简化的方法来求解逆矩阵,它比求解16个等式的方法快很多,后面会介绍快速求解逆矩阵的做法。

程序3:glib/Gut.cpp

//eye=镜头位置,lookat=镜头对准的位置,up=镜头正上方的方向
Matrix4x4 GutMatrixLookAtRH(Vector4 &eye, Vector4 &lookat, Vector4 &up) { //找出镜头的后方、右方、上方的3个轴向,右手坐标系把-z视为前方 Vector4 up_normalized = VectorNormalized(up);
Vector4 zaxis = (eye - lookat);zaxis.Normalized();//镜头法线的反向,也就是后方
Vector4 xaxis = Vector3CrossProduct(up_normalized, zaxis);//镜头右方
Vector4 yaxis = Vector3CrossProduct(zaxis, xaxis); //镜头上方

Matrix4x4 matrix; matrix.Identity();
//把矩阵左上角3*3的部分设置成镜头的3个轴向
matrix[0] = xaxis;
matrix[1] = yaxis;
matrix[2] = zaxis;
//左下角直接带入镜头位置
matrix[3] = eye;
//对矩阵做逆矩阵,镜头转换矩阵满足了使用快速逆矩阵计算的条件。
matrix.FastInvert();
return matrix; }

应用

在理解了如何移动向量以及如何改变坐标参考点的方法后,我们就有足够的知识来创建范例程序中常看到的world matrix和view matrix。world matrix会对向量做移动,把物体转换到世界坐标系中。view matrix被设置成镜头移动矩阵的逆矩阵,它不是用来移动向量的,而是用来计算向量相对于镜头的位置。

如果矩阵左上角的3*3旋转部分的3个轴向都是单位向量,并且它们都两两互相垂直,就可以使用一个更快速的方法来求其逆矩阵,而不需要求解16个未知数。只要把矩阵左上方的3*3部分转置,就可以得到逆矩阵左上角的3*3部分;再把左下角的1*3部分政府相反,然后把它与转置后的3*3矩阵相乘,就可以得到逆矩阵左下角的1*3部分;剩下的右端4*1部分则不需要做任何改变,它们永远都是(0,0,0,1)。下面列出求解这一类矩阵的逆快速阵的计算方法。

投影

前面所介绍的坐标转换比较接近于正规线性代数中所使用的计算。本节补充一些只有在3D绘图中才会看到的线性代数应用,主要是投影矩阵的部分。

Direct3D的屏幕坐标系范围是(-1,-1,0)~(1,1,1),X和Y轴范围都是-1~1,而Z轴范围是0~1。OpenGL的屏幕坐标系范围则(-1,-1,-1)~(1,1,1),它的Z轴范围比Direct3D大。3D绘图中的可视对象都要想办法通过坐标转换把它们挤进屏幕坐标系的可视范围内。

在Fixed Function Pipeline中,Direct3D和OpenGL都定义了Projection Matrix。这个矩阵的作用就是把镜头可视范围内的3D对象挤进屏幕坐标系中。如果镜头使用的是透视图,投影矩阵还会提供对象大小随距离变化的信息。投影矩阵Projection Matrix是比较特别的坐标转换,它不只是简单地与向量相乘,还要对相乘结果做一些处理才会得到最终结果。

先来看比较简单的正交视图投影。正交视图下的3D对象并不会随着距离改变大小。所以正交视图的投影只是很简单的缩放计算,也就是对镜头坐标系上的顶点在X、Y、Z三个轴上做线性缩放。假设有一个正交视图镜头,它在水平方向上的可视范围为10个单位,垂直方向上的可视范围为8个单位;投影矩阵所要做的工作就是把镜头坐标上的顶点位置向量X乘以1/5,Y乘以1/4;让X值在-5~5之间且Y值在-4~4之间的向量可以落入-1~1的屏幕可视范围内。

正交视图的Z值转换比较复杂,镜头设置中需要用到Znear和Zfar这两个数值,它们分别代表镜头所能看到的最近和最远的位置。投影矩阵的Z值转换,除了要对Z做缩放之外,还需要用到平移,让Z轴上距离镜头为Znear的点转换为0,距离为Zfar的点转换成1。

下面列出Direct3D版本的正交投影矩阵计算公式,这个公式把这两段文字描述转换成数学公式,它是以右手坐标系为标准来创建矩阵的。

《3D绘图程序设计》彭国伦

左上方3*3矩阵可以对向量进行缩放。左下角的1*3矩阵看上去好像是对向量的平移,不过仅仅是对z的平移。所以这个矩阵对x和y进行缩放,对z方向进行缩放和平移。

OpenGL所使用的投影矩阵在Z值转换上有点不同,因为它的Z轴可视范围是-1~1。

《3D绘图程序设计》彭国伦

透视图投影Perspective Projection比较复杂,它除了要缩放3D向量外,还要根据向量到镜头的距离来改变3D向量缩放的比例。简单地说,它除了要实现正交视图矩阵的功能外,还要能够根据向量到镜头的距离来改变缩放比例。简单地说,它除了要实现正交视图矩阵的功能外,还要能够根据向量到镜头的距离来改变缩放比例。只靠向量和矩阵相乘,并不能做到这个效果;相乘后的新向量中的W值就是缩放变化值,把整个向量再除以W所得到的就是透视图中的投影结果。

先把向量和矩阵相乘。

V*M=T

再把计算结果向量T除以Tw,所得到的向量P就是透视投影的结果。(结果向量T中除了x、y、z还有一个就是w)

[Tx/Tw   Ty/Tw   Tz/Tw    Tw/Tw] = [Px Py Pz 1]

硬件会自动做第2步除以Tw的操作,这点在使用Fixed Function Pipeline和Shader时都一样,写程序时可以省略这步。在创建透视图投影矩阵时,它与正交投影矩阵不同的是,设置透视图投影矩阵经常是以镜头的视角为参数,再配合屏幕的长宽比例来决定X和Y轴上的缩放比例。

下面先列出Direct3D所使用的投影矩阵计算公式。读者可以发现,根据这个式子所得到的W值其实就是顶点到镜头间在Z值上的距离,W值越大代表顶点离镜头越远,越大的W值会把向量缩的越小。

《3D绘图程序设计》彭国伦

下面是OpenGL版本所使用的公式,请注意它对Z值的转换不太相同,OpenGL的屏幕坐标Z值范围是-1~1。

《3D绘图程序设计》彭国伦

编写Shader时,有时需要把向量从屏幕坐标系转换回镜头坐标系或者世界坐标系上。范例程序chap15/DepthOfField就用到了这个计算,不过它只需要用到转换到镜头坐标系后的Z值,所以其计算可以简化。

《3D绘图程序设计》彭国伦

在Pixel Shader中很容易就可以知道向量P的内容,(X,Y)部分就是像素在屏幕上的位置,Z可以从ZBuffer中获得,W可以直接设置成1。有了向量P之后,只要带入正确的逆矩阵,就可以把P再还原到其他坐标系中。例如,只要配合Mp的逆矩阵,就可以把它还原到镜头坐标系上,也就是chap15/DepthOfField所需要的数据。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>]

 

第三章 动画和交互

第2章的范例程序画模型的方法并不是最有效率的,本章会介绍比较好的做法。到目前为止,所看过的范例程序都只画出静态的图,本章开始介绍具备动画及交互功能的范例程序。

Direct3D9定义了3个坐标转换矩阵,第2章的范例使用了其中两种矩阵,D3DTS_VIEW镜头转换矩阵和D3DTS_PROJECTION投影转换矩阵,有了这两个矩阵就可以把3D顶点转换到屏幕坐标系。实现3D绘图时,经常还会用到第3种矩阵来移动对象,它通常被称为World Matrix,也就是世界坐标转换矩阵。

假设要画出4个形状和大小完全相同的金字塔,在创建模型数据时,可以把这4组金字塔的每个顶点坐标都定位出来,在内存中存放8*4=32个顶点,来创建4个不同的金字塔。这并不是最好的方法,既然已经知道这4个金字塔的形状和大小是完全相同的, 就可以使用不同的转换矩阵,把相同的金字塔模型放在不同位置上来绘图。范例程序在画这四个金字塔时,镜头位置保持不变,投影转换矩阵也保持不变,唯一改变的只是移动world_matrix,在此需要使用4个不同的world_matrix分别画出4个金字塔。下面先来看一段伪代码。

/*
  object_position = 金字塔的某个顶点
  world_matrix = 位移转换矩阵,也就是世界坐标转换矩阵
  view_matrix = 镜头坐标转换矩阵
  projection_matrix = 投影转换矩阵
*/
//把顶点乘以位移转换矩阵,可以得到它在世界坐标系上的位置
world_position = object_position * world_matrix
//把转换到世界坐标系的顶点乘以镜头转换矩阵,得到它相对于镜头的位置
view_position = world_position * view_matrix
//把转换到镜头坐标系位置的点乘以投影转换矩阵,可以得到它在屏幕坐标系的位置
screen_position = view_position * projection_matrix

前面提到过,计算坐标转换时,向量并不需要和这么多个矩阵相乘,我们可以预先把所需要用到的矩阵都乘起来,顶点再直接和组合后的单一矩阵相乘,所得到的结果是相同的。如果我们只是为了做到3D绘图的屏幕坐标转换,那就把前面所提到的3种矩阵相乘,再把结果传给Direct3D和OpenGL就足够了。Direct3D和OpenGL把转换矩阵分成两三个不同的矩阵来设置,这是因为要处理光照。如果不需要处理光照,只需要一个矩阵就够了,下一章会介绍光照。下面是一个利用矩阵来移动对象的范例,如图3.1所示。

《3D绘图程序设计》彭国伦

//移动3D对象,复制4个金字塔
void RenderFrameOpenGL2(void)
{
    //清除画面
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    Matrix4x4 g_view_matrix = GutMatrixLookAtRH(g_eye, g_lookat, g_up);
    //Matrix4x4 perspective_matrix = GutMatrixPerspectiveRH_OpenGL(90.0f, 1.0f, 1.0f, 100.0f);
    Matrix4x4 projection_matrix = GutMatrixPerspectiveRH_OpenGL(60.0f, 1.0f, 1.0f, 100.0f);
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf((float*)&projection_matrix);
    //Matrix4x4 view_perspective_matrix = view_matrix * perspective_matrix;
    //设置好GPU要去哪里读取顶点数据
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(4, GL_FLOAT, sizeof(Vector4), g_vertices);
    //设置要改变GL_MODELVIEW矩阵
    glMatrixMode(GL_MODELVIEW);

    //4个金字塔的位置
    Vector4 pos[4] =
    {
        Vector4(-1.0f,-1.0f,0.0f),
        Vector4(1.0f, -1.0f,0.0f),
        Vector4(-1.0f,1.0f,0.0f),
        Vector4(1.0f,1.0f,0.0f)
    };
    Matrix4x4 world_matrix;
    Matrix4x4 world_view_matrix;

    for (int i=0;i<4;i++)
    {
        //得到位移矩阵
        world_matrix.Translate_Replace(pos[i]);
        world_view_matrix = world_matrix * g_view_matrix;
        //设置转换矩阵
        glLoadMatrixf((float*)&world_view_matrix);
        //画出金字塔的8条边线
        glDrawArrays(GL_LINES, 0, 16);
    }
}

坐标转换矩阵除了可以移动对象之外,还可以用来实现对象旋转。只要计算出旋转矩阵,把它传给OpenGL,经过坐标转换后就可以看到旋转效果。Matrix4x4对象提供了几个函数用来计算旋转矩阵,RotateX,RotateY和RotateZ可以用来计算对X、Y和Z轴三个轴旋转的矩阵,它们分别都有字尾接上_Replace的版本,用来直接替换原来矩阵的内容,省略了相乘的动画,生成对世界坐标系X,Y和Z轴旋转的矩阵。

下面所列出的旋转矩阵生成公式都是使用右手坐标系,旋转是以逆时针方向为正值。读者也许在其他地方看到正负号相反的式子,它可能是用左手坐标系,以顺时针方向为正值定义旋转角度,这并没有关系。本书统一使用右手坐标系来介绍3D绘图。再提醒一次,在本书所列出的公式中,矩阵排列方法是按照C++的二维数组排列方法,如果在其他地方看到不同排列方法,不要慌张,把它做个转置Transpose结果就会相同了。

公式:对X轴旋转的矩阵

《3D绘图程序设计》彭国伦

公式:对Y轴旋转的矩阵

《3D绘图程序设计》彭国伦

 公式:对Z轴旋转的矩阵

《3D绘图程序设计》彭国伦

到目前为止已经出现了好几个金字塔范例程序的版本,它们并不是来创建模型数据的最好方法。顶点数组总共有16个顶点,分别记录8条边的两个端点,仔细观察就会发行有很多顶点是重复出现的。接下来的范例使用另一种方法来创建模型,金字塔的5个顶点不会重复出现在内存中,使用另外一个记录顶点编号和排列方法的索引列表IndexArray,用来指定每条线段是由哪两个顶点组成。

第八个程序:chap03/rotation/render_data.cpp

// 金字塔形的5个顶点
Vector4 g_vertices[5] = 
{
    Vector4(-1.0f, 1.0f,-1.0f),
    Vector4(-1.0f,-1.0f,-1.0f),
    Vector4( 1.0f,-1.0f,-1.0f),
    Vector4( 1.0f, 1.0f,-1.0f),
    Vector4( 0.0f, 0.0f, 1.0f),
};

// 链接出金字塔8条边线的索引值
unsigned short g_indices[16] =
{
    0, 1,
    1, 2,
    2, 3,
    3, 0,
    0, 4,
    1, 4,
    2, 4,
    3, 4
};

索引值一般都是使用2 Bytes的unsigned short类型来存储,使用4 Bytes的整数来存储也可以,只是通常不需要。使用新方法存储是原来的存储量一半还少。既然使用了新的方法来存储数据,那么也应该改用其他函数来绘图。范例程序chap03/rotation画一个在屏幕上慢慢旋转的金字塔,书中只能给出静态的结果,如图3.2所示。

绘图部分的代码和前几个范例几乎相同,差别在于所生成的是旋转矩阵,还有要调用不同的函数来使用索引值数组Index array。在此只给出有差别的代码。

第九个程序:chap03/rotation/render_opengl.cpp

// `旋转角度`
static float angle = 0.0f;
angle += 0.01f;
// `设置旋转矩阵`
Matrix4x4 world_view_matrix = g_view_matrix;
world_view_matrix.RotateZ(angle);
glLoadMatrixf( (float *) &world_view_matrix);
// `画出金字塔的8条边线`
glDrawElements(
  GL_LINES,    // `指定所要画的基本图形种类`
  16,            // `有几个索引值`
  GL_UNSIGNED_SHORT,    // `索引值的类型`
  g_indices    // `索引值数组`
);

矩阵除了可以控制位移和旋转之外,还可以通过它来把对象拉长或者压扁,能够做到重复使用相同的模型画出高低肥瘦不同的物体。下面的范例程序会画出4个位置和大小不同的金字塔,如图3.3所示,它同样是使用相同的模型数据简单地通过控制矩阵的方法来改变金字塔的外观。在此会用到4个不同的转换矩阵来画出4个金字塔,g_position中存放了4个位移值,g_scale中有4个记录对象缩放大小的值。计算矩阵时,先调用Scale_Replace创建缩放矩阵,再直接把位移填入这个矩阵的左下角。

公式:缩放矩阵

《3D绘图程序设计》彭国伦

 公式:缩放和位移合并的矩阵

《3D绘图程序设计》彭国伦

 范例chap03/scale和chap03/instance的代码两者大同小异,这里的镜头位置被重新设置过,书中只节选了两者之间有差别的部分。

第十个程序:chap03/scale/render_opengl.cpp

 

//缩放3D对象
void RenderFrameOpenGL(void)
{
    // 金字塔形的5个顶点
    Vector4 g_vertices[5] =
    {
        Vector4(-1.0f, 1.0f, 0.0f),
        Vector4(-1.0f,-1.0f, 0.0f),
        Vector4(1.0f,-1.0f, 0.0f),
        Vector4(1.0f, 1.0f, 0.0f),
        Vector4(0.0f, 0.0f, 1.0f),
    };
    // 链接出金字塔8条边线的索引值
    unsigned short g_indices[16] =
    {
        0, 1,
        1, 2,
        2, 3,
        3, 0,
        0, 4,
        1, 4,
        2, 4,
        3, 4
    };
    // 4个金字塔的位移
    Vector4 g_position[4] =
    {
        Vector4(-2.0f,-2.0f, 0.0f),
        Vector4(2.0f,-2.0f, 0.0f),
        Vector4(-2.0f, 2.0f, 0.0f),
        Vector4(2.0f, 2.0f, 0.0f),
    };
    // 4个金字塔的缩放值
    Vector4 g_scale[4] =
    {
        Vector4(1.0f, 1.0f, 1.0f),
        Vector4(1.0f, 1.5f, 4.0f),
        Vector4(1.0f, 1.0f, 2.0f),
        Vector4(1.5f, 1.5f, 3.0f),
    };
    // 镜头位置
    Vector4 g_eye(0.0f, 8.0f, 1.0f);
    // 镜头对准的点
    Vector4 g_lookat(0.0f, 0.0f, 1.0f);
    // 镜头正上方的方向
    Vector4 g_up(0.0f, 0.0f, 1.0f);
    // 计算出一个可以转换到镜头坐标系的矩阵
    Matrix4x4 g_view_matrix = GutMatrixLookAtRH(g_eye, g_lookat, g_up);
    // 投影矩阵
    Matrix4x4 projection_matrix = GutMatrixPerspectiveRH_OpenGL(90.0f, 1.0f, 1.0f, 100.0f);
    // 设置视角转换矩阵
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf((float *)&projection_matrix);
    // 清除画面
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // 设置好GPU要去哪读取顶点数据
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(4, GL_FLOAT, sizeof(Vector4), g_vertices);
    // 设置要改变GL_MODELVIEW矩阵
    glMatrixMode(GL_MODELVIEW);
    for (int i = 0; i<4; i++)
    {
        // `创建转换矩阵`
        Matrix4x4 world_matrix;
        world_matrix.Scale_Replace(g_scale[i]); // `创建缩放矩阵`
        world_matrix[3] = g_position[i]; // `直接把位移填入矩阵左下角.`

                                         // `设置转换矩阵`
        Matrix4x4 world_view_matrix = world_matrix * g_view_matrix;
        glLoadMatrixf((float *)&world_view_matrix);

        // 画出金字塔的8条边线
        glDrawElements(
            GL_LINES, // 指定所要画的基本图形种类
            16, // 有几个索引值
            GL_UNSIGNED_SHORT, // 索引值的类型
            g_indices // 索引值数组
        );
    }
}

《3D绘图程序设计》彭国伦

 

原文链接: https://www.cnblogs.com/2008nmj/p/17018528.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    《3D绘图程序设计》彭国伦

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/309029

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 上午10:48
下一篇 2023年2月16日 上午10:49

相关推荐