在计算机三维世界中,想要模拟出真实的物体,让它的表面看起来更加逼真,就需要使用“纹理映射”技术,简单讲就是一种将2D图像映射到3D物体上面。一般来说,纹理是表示物体表面细节的一幅或多幅2D图像,也称为纹理贴图。当我们把纹理贴图按照特定方式映射到物体表面上的时候,能够使得物体看上去更加逼近现实。其实,我们可以把纹理看做应用到物体表面上的像素颜色即可。

Direct3D纹理贴图支持多个格式的图像,包括.jpg,.bmp,.dds,.png等等,并且图像的尺寸一般都是边长为2的N次幂正方形图片,比如128*128,256*256,512*512,1024*1024等等。纹理贴图通过一个二维数组存储每个点的颜色值,该颜色值被称为纹理元素。每个纹理元素在纹理坐标系中都有一个唯一的二维坐标,为了将纹理贴图映射到三维模型中,Direct3D在顶点结构中使用对应的纹理坐标来确定纹理贴图上的每个纹理元素。纹理坐标由一个二维坐标系指定,这个坐标系由沿着水平方向的u轴和垂直方向的v轴构成,他们两个合起来就是一对坐标(u,v),其实和我们习惯的(x,y)坐标对是一个概念,只是表达方式不同而已。纹理坐标的取值方位在[0,1]之间,左上角是(0,0),右下角是(1,1)。这也是为什么纹理贴图为什么是正方形的一个原因。

为了演示纹理映射,我们使用VS2019创建一个新项目“D3D_05_Texture”,将之前的代码复制过来,首先我们声明全局变量,代码如下:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 定义FVF灵活顶点格式结构体
struct D3D_DATA_VERTEX { FLOAT x, y, z, u, v; };// 定义包含纹理的顶点类型
#define D3D_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)// 顶点缓冲区对象
LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer = NULL;// 纹理对象
LPDIRECT3DTEXTURE9 D3DTexture = NULL;

请注意,我们在定义顶点结构和类型的时候,都包含了纹理坐标(u,v)。为了简单说明纹理的使用方式,本案例中,我们创建一个四边形,并将一张尺寸为512*512的图像赋予这个四边形上面。因为四边形正好对应正方形图像,也就是说四边形左上角顶点的UV坐标是(0,0),四边形右下角顶点的UV坐标是(1,1)。这样的话,正方形图像正好平铺在四边形上面。那么,首先我们需要在initScene函数中来组织这个四边形顶点数据,代码如下:

// 初始化四边形顶点数组数据
//
//  V1 *********** V2
//      *       * *
//      *     *   *
//      *   *     *
//      * *       *
//  V0 *********** V3
//
D3D_DATA_VERTEX vertexArray[] =
{// 三角形V0V1V2,左下角顺时针{ -10.0f,  -10.0f, 0.0f, 0.0f, 1.0f },   // 左下角UV坐标(0,1){ -10.0f,  10.0f, 0.0f, 0.0f, 0.0f },    // 左上角UV坐标(0,0){  10.0f,  10.0f, 0.0f, 1.0f, 0.0f },    // 右上角UV坐标(1,0)// 三角形V0V2V3,左下角顺时针{ -10.0f,  -10.0f, 0.0f, 0.0f, 1.0f }, // 左下角UV坐标(0,1){  10.0f,  10.0f, 0.0f, 1.0f, 0.0f },    // 右上角UV坐标(1,0){  10.0f,  -10.0f, 0.0f, 1.0f, 1.0f },   // 右下角UV坐标(1,1)
};// 创建顶点缓冲区对象
D3DDevice->CreateVertexBuffer(sizeof(vertexArray),0,D3D_FVF_VERTEX,D3DPOOL_DEFAULT,&D3DVertexBuffer,NULL);// 填充顶点缓冲区对象
void* ptr;
D3DVertexBuffer->Lock(0,sizeof(vertexArray),(void**)&ptr,0);
memcpy(ptr, vertexArray, sizeof(vertexArray));
D3DVertexBuffer->Unlock();

我们要对物体表面进行纹理映射的话,首先要创建纹理对象,我们可以使用D3DX库中的D3DXCreateTexture函数创建一个纹理对象。但是是更多情况是从文件中读取纹理,也就是使用D3DXCreateTextureFromFile方法,此函数还有另一个加强版本D3DXCreateTextureFromFileEx,它不仅可以从图像重加载并创建纹理对象,还能指定纹理对象的宽度,高度等等属性信息。生成纹理对象后,就可以调用SetTexture方法,设置当前需要启用的纹理。创建纹理对象的过程,我们一般放在initScene函数中,而给模型设置纹理的操作是放在模型渲染之前,也就是renderScene函数中。本案例使用,我们组织完四边形顶点数据后,就使用D3DXCreateTextureFromFile来从当前项目目录下加载一个名为rocks.jpg的图片文件作为纹理对象。代码如下:

// 创建纹理对象
D3DXCreateTextureFromFile(D3DDevice, L"rocks.jpg", &D3DTexture);

这里要注意的就是,名称为rocks.jpg的图片文件是在当前项目路径下,也就是和我们的源代码文件main.cpp在一起的。如果是其他目录下的话,我们可以使用绝对路径来读取。接下来的initScene函数中就是初始化投影变换和光照的函数调用,代码如下:

// 初始化投影变换
initProjection();// 初始化光照
initLight();

至此,initScene函数就已经完毕了。下面的介绍initProjection函数,我们直接固定投影变换即可,代码如下:

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(0.0f, 0.0f, -20.0f);
D3DXVECTOR3 viewLookAt(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);// 设置透视投影变换矩阵
D3DXMATRIX projMatrix;
float angle = D3DX_PI * 0.5f;
float wh = (float)WINDOW_WIDTH / (float)WINDOW_HEIGHT;
D3DXMatrixPerspectiveFovLH(&projMatrix,angle,wh,1.0f,1000.0f);
D3DDevice->SetTransform(D3DTS_PROJECTION, &projMatrix);// 设置视口变换
D3DVIEWPORT9 viewport = {0,0,WINDOW_WIDTH,WINDOW_HEIGHT,0,1};
D3DDevice->SetViewport(&viewport);

接下来就是光照的初始化工作,我们简单置一个全局环境光即可,同时设置一个默认的材质,该材质中只反射环境光。代码如下:

// 设置一下环境光
D3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(255, 255, 255));// 开启光照
D3DDevice->SetRenderState(D3DRS_LIGHTING, true);// 设置默认材质
D3DMATERIAL9 defaultMaterial;
::ZeroMemory(&defaultMaterial, sizeof(defaultMaterial));
defaultMaterial.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);   // 100%反射环境光
defaultMaterial.Diffuse = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);   // 不反射漫反射光
defaultMaterial.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);  // 不反射高光
defaultMaterial.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);  // 不自发光
defaultMaterial.Power = 0.0f;                                  // 没有高光区
D3DDevice->SetMaterial(&defaultMaterial);

最后就是我们的renderScene函数,代码如下:

// 设置纹理
D3DDevice->SetTexture(0, D3DTexture);// 绘制四边形
D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_DATA_VERTEX));
D3DDevice->SetFVF(D3D_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);

运行代码,效果如下:

我们总结一下, 纹理的使用其实非常的简单,就是指定顶点的纹理坐标,然后从一张图片中加载到纹理对象,最后渲染图形的时候,给该图形设置好纹理对象即可。当然,这只是简单纹理映射使用案例,对于一些复杂的3D模型,它的纹理映射并不是在代码中指定的,而是通过建模软件完成的。例如在3ds max中就可以使用“UV展开”或者“UV Map”来对复杂的模型(人物模型)进行UV映射处理。因此,我们在加载3D模型文件的时候,模型中已经包含了UV坐标数据了,我们只需要加载模型对应的纹理贴图文件,然后在渲染模型的时候,使用该纹理即可。这个过程基本都是一致的。纹理映射的关键在贴图UV和模型UV的对应关系。简单的我们可以自己手动代码来完成,复杂的则是有建模软件完成。

我们之前讲过,DirectX中绘制图形的尺寸并不是像素单位,由于摄像机的关系,它在屏幕上显示的尺寸可能大,也可能小。但是,我们的纹理贴图的大小是固定的像素尺寸。例如本案例中,我们使用的就是512*512像素大小尺寸JPG图片。如果四边形在屏幕上的尺寸大于或者小于纹理贴图的尺寸时候,可能会出现一些问题。大多数情况下,屏幕显示的图形大小和纹理贴图大小是不同的。也就是说纹理图像可能会被放大或缩小。多纹理放大会造成屏幕上的多个像素被映射同一个纹理元素(纹理元素不够),这样三维模型渲染出来就会有色块的感觉。缩小纹理就会造成同一个像素被映射到许多纹理元素(纹理元素太多),这样三维模型渲染出来也会失真或有锯齿。为了解决上述问题,我们需要将多个纹理元素的颜色按照不同方式融合到一个像素上,这种称之为纹理过滤方式。在Direct3D中,有四种纹理过滤方式:最近点采样过滤,线性纹理过滤,各项异性过滤和多级渐进过滤。每种过滤方式都有自己的优缺点。设置纹理过滤方式通常调用SetSamplerState函数,函数原型如下:

HRESULT SetSamplerState (DWORD Sampler,              // 指定纹理层次,取值0-7即可。D3DSAMPLERSTATETYPE Type,  // 纹理采样类型DWORD Value                    // 纹理采样参数值
);

最近点采样过滤,速度最快但是效果最差,代码设置如下:

// 最近点采样
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT);
D3DDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_POINT);

线性纹理过滤是目前使用最广泛的纹理过滤方式。与最近点采样方式相比,能有效的提高图像的显示质量,且对系统性能影响不大。它就是将目标纹理元素与最接近的上下左右4个纹理元素进行加权平均,从而得到最终显示的颜色值。代码如下:

// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

各向异性纹理过滤,该过滤方法可以得出最好的效果,同时会消耗相当多的计算时间,代码如下设置:

// 各向异性纹理过滤
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC);
D3DDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_ANISOTROPIC);
D3DDevice->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, 4);

在本案例中,我们就使用线性纹理即可,这段代码放置在创建纹理对象的下面即可。

多级渐进过滤:就是为纹理创建一系列具有不同尺寸的纹理图像。多级渐进过滤就是由一组分辨率逐渐降低的纹理序列组成,每一级纹理的尺寸都是上一级的一半。Direct3D在纹理映射时候,自动选择一幅与物体大小最接近的纹理进行映射。因此我们一般会创建一个分辨率最高的纹理,然后利用多级渐进过滤方式产生多组纹理。多级渐进过滤能够有效的提高图形渲染速度。其实多级渐进过滤也是为了解决纹理贴图尺寸的问题。

寻址模式:Direct3D可以为任何图元的顶点指定纹理坐标,且纹理坐标取值范围是[0,1]。但是在实际过程中,这种取值范围不能满足我们的要求。Direct3D有四种纹理模式,来处理超过[0,1]范围之外的情况。它们分别是重复寻址模式,镜像纹理寻址模式,夹取纹理寻址模式和边框颜色纹理寻址模式。重复寻址模式是Direct3D中默认的寻址模式,这种寻址模式允许在每个整数链接点处重复上一个整数的纹理。比如,我们绘制一个正方形,指定4个顶点的纹理坐标分别是(0,0),(0,2),(2,2),(2,0),那么Direct3D就会在u,v方向各赋值两遍原始纹理。也就是说,在四方形上展示4个大小的原始纹理贴图。镜像纹理寻址模式则是在每个整数纹理坐标连接处自动复制并翻转纹理。夹取寻址模式则是将超过1的部分将边缘的内容沿着u,v轴延伸。边框颜色纹理寻址则是草果1的部分就用边框颜色填充了。

多重纹理:就是在一个模型表面附加两张以上纹理贴图。两张纹理贴图的像素根据一定的计算公式得到最终的像素值,并施加到模型顶点上。我们之前讲的“光照贴图”就是利用该技术来实现的。多重纹理的本质就是像素间的公式计算。我们使用VS2019创建一个新项目“D3D_05_Multitexture”,并复制上个项目的代码。这次我们准备两种纹理贴图,如下:

我们会将这两张纹理贴图同时赋予一个四边形上面。首先,我们需要声明四边形的顶点支持两个UV坐标,然后声明两个纹理对象:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 定义FVF灵活顶点格式结构体
struct D3D_DATA_VERTEX { FLOAT x, y, z; DWORD color; FLOAT u1, v1, u2, v2; };// 定义包含两个纹理的顶点类型
#define D3D_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 | D3DFVF_TEX2)// 顶点缓冲区对象
LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer = NULL;// 两个纹理对象
LPDIRECT3DTEXTURE9 D3DTexture1 = NULL;
LPDIRECT3DTEXTURE9 D3DTexture2 = NULL;

虽然,我们在顶点中使用两个纹理,但是由于都是四边形,因此他们的纹理坐标其实是一样的。我们在initScene函数中重新调整四边形的顶点数据,顶点数组数据如下:

// 三角形V0V1V2,左下角顺时针
{ -10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 0.0f, 1.0f, 0.0f, 1.0f },   // 左下角UV坐标(0,1)
{ -10.0f,  10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 0.0f, 0.0f, 0.0f, 0.0f },   // 左上角UV坐标(0,0)
{  10.0f,  10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 1.0f, 0.0f, 1.0f, 0.0f },   // 右上角UV坐标(1,0)// 三角形V0V2V3,左下角顺时针
{ -10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 0.0f, 1.0f, 0.0f, 1.0f },   // 左下角UV坐标(0,1)
{  10.0f,  10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 1.0f, 0.0f, 1.0f, 0.0f },   // 右上角UV坐标(1,0)
{  10.0f, -10.0f, 0.0f, D3DCOLOR_XRGB(255,255,255), 1.0f, 1.0f, 1.0f, 1.0f },   // 右下角UV坐标(1,1)

虽然我们定义了定的颜色,但是它只是用来参与像素公式计算的,最终四边形的颜色还是由两种图片决定。然后,我们创建两个纹理对象,也就是加载上面两张照片。代码如下:

// 创建两个纹理对象
D3DXCreateTextureFromFile(D3DDevice, L"texture1.jpg", &D3DTexture1);
D3DXCreateTextureFromFile(D3DDevice, L"texture2.jpg", &D3DTexture2);

接下来就是我们的renderScene函数,这个函数中,我们要对两个纹理对象进行计算。本案例中,我们的计算公式是乘法,也就是将两个像素相乘后赋予四边形。当然,我们还可以使用加法,不同的公式获得的视觉效果是不一样的。这里我们不需要纠结代码的实现,主要是了解多重纹理的本质就是将贴图像素按照一定的算法获取最终的效果。本案例代码如下:

// 设置第一个要混合的纹理
D3DDevice->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);
// 混合公式为乘法(加法)
D3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTEXOPCAPS_MODULATE);
//D3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_ADD);
// 纹理颜色与顶点颜色相乘(结果一)
D3DDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
D3DDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);// 设置第二个要混合的纹理
D3DDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 1);
// 混合公式为乘法(加法)
D3DDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTEXOPCAPS_MODULATE);
//D3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_ADD);
// 将上一个混合颜色(结果一)和本纹理颜色相乘
D3DDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
D3DDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);// 设置纹理,注意索引
D3DDevice->SetTexture(0, D3DTexture1);
D3DDevice->SetTexture(1, D3DTexture2);// 绘制四边形
D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_DATA_VERTEX));
D3DDevice->SetFVF(D3D_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);

至此,多重纹理完毕,运行查看效果,如下:

透明纹理图片:我们日常使用的透明图片基本都是PNG格式的。但是这种格式的图片在用作纹理贴图的时候,运行的效果并不是透明的。DirectX中要显示透明效果的话,就必须要求你的纹理贴图包含Alpha通道。该通道中存储了图像中每个像素对应的alpha值,它的取值范围是0 - 255,就是从完全透明(0)到不透明(255)的一个过渡值。当然,128就代表半透明了。其实,我们可以理解Alpha通道就是和原图像一样大小的灰度图,这张灰度图上面的每个像素值就是alpha值,也就是0-255之间。如果想让原图像的一部分内容不显示的话,那么这个部分内容对应的alpha值应该是0,Alpha通道中表现出来就是黑色区域。

这是一张BMP格式的纹理贴图,该文件格式可以保存Alpha通道。我们可以在PS中看到它的Alpha通道,其实就是一张黑白图像。我们只想让有颜色的区域显示处理,那些空白的区别要进行完全透明处理。也就是说,在Alpha通道中,那些空白区域应该对应为黑色。我们使用VS2019创建新项目“D3D_05_Transparent”,并复制之前“D3D_05_Texture”项目代码。我们的代码改动非常少,首先是图片名称变了,那么创建纹理对象也跟随调整一下,

// 创建纹理对象
D3DXCreateTextureFromFile(D3DDevice, L"sunwukong.bmp", &D3DTexture);

接下来就是我们的renderScene函数,在这个函数中,我们需要开启Alpha融合,然后设置源像素和目标像素,以及运算方式,这些基本都是固定不变的。最后就是设置纹理并绘制。代码已经给了一部分注释,大家了解即可。

// 开启Alpha融合
D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);// 设置混合因子
// 透明物体是源像素,因此它使用自己的alpha值
// 不透明的物体是目标像素,因此它使用源像素的相反alpha值,就是不透明
D3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
D3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);// 设置融合运算方式(相加)
D3DDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);// 设置纹理,注意索引
D3DDevice->SetTexture(0, D3DTexture);// 绘制四边形
D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_DATA_VERTEX));
D3DDevice->SetFVF(D3D_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);// 关闭Alpha混合
D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);

运行效果如下: 

我们可以看到,我们的透明图片得到了正确的显示。这里大家一定要清楚,我们日常使用的PNG透明图片直接作为纹理使用,是没有透明效果的。我们必须使用包含透明通道的图片才行,比如bmp格式,tga格式等等。如何将一个透明的PNG格式图片转化成包含透明通道的bmp格式,我们会在游戏美术课程中介绍,这里不在详细叙述。

如何让模型透明化显示,其他它的原理就是利用纹理透明。因为我们知道,模型本身就是一副空壳而已,表面纹理透明的话,这个模型就透明了。我们知道模型的颜色源于材质的漫反射,在材质中是可以设置漫反射的alpha值的,然后在渲染模型的时候,开启透明融合,就能实现模型透明的效果了。如何开启透明融合,我们已经上文讲过了。如何设置材质的漫反射alpha值,代码如下:

// 设置半透明!!!
D3DMaterials[i].Diffuse.a = alpha;

这里我们不在介绍具体的代码细节。该项目的名称为“D3D_05_CubeTexture”。在本项目中,我们分别封装了“Quad”四边形类,“Cube”立方体类和“Mesh”网格类,以及如何在main.cpp中使用它们。关于网格的代码,我们会在第八章中详细介绍。需要注意一点,在使用透明混合是,一定要先渲染不透明物体,再渲染透明物体。

GUI:游戏开发中经常会使用的用户界面,这些基本上就是使用四边形纹理贴图实现的。这些用户界面可以响应键盘或鼠标事件,也就是和用户进行交互。接下来,我们将完成一个简单的用户界面,主要是一个背景和两个按钮,同时支持用户鼠标点击操作。在使用四边形构建用户界面的时候,我们通常使用D3DFVF_XYZRHW作为顶点类型,也就是相对于窗体的坐标系,这种方式构建的四边形不需要投影变换,就能直接显示在窗体上面。我们使用VS2019创建一个新项目“D3D_05_GUI”,并复制之前的代码。从本案例开始,我们将逐步的对各种操作进行封装,也就是说,我们要讲GUI封装成一个简单的类,然后在main.cpp中使用。这样,我们除了要创建main.cpp主源码文件之外,还要创建gui.h和gui.cpp文件。同时,为了能够将头文件的引入公共化,我们创建main.h文件,里面内容如下:

#pragma once
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>// 引入依赖的库文件
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")#define WINDOW_LEFT       200             // 窗口位置
#define WINDOW_TOP      100             // 窗口位置
#define WINDOW_WIDTH    800             // 窗口宽度
#define WINDOW_HEIGHT   600             // 窗口高度
#define WINDOW_TITLE    L"D3D游戏开发"    // 窗口标题
#define CLASS_NAME      L"D3D游戏开发"    // 窗口类名

接下来,我们来看gui.h文件的内容:

#include "main.h"// 定义FVF灵活顶点格式结构体,使用像素坐标
struct D3D_GUI_DATA_VERTEX { FLOAT x, y, z, rhw, u, v; };// 定义包含纹理的顶点类型
#define D3D_GUI_FVF_VERTEX (D3DFVF_XYZRHW | D3DFVF_TEX1)// 定义GUI类,其实就是一个四边形纹理
class Gui {public:float posX, posY;     // GUI控件位置float width, height;  // GUI控件尺寸const wchar_t* txt;       // GUI控件文本const wchar_t* bg;        // GUI控件背景// GUI控件纹理对象LPDIRECT3DTEXTURE9 texture = NULL;// GUI控件顶点缓冲区对象LPDIRECT3DVERTEXBUFFER9 buffer = NULL;// 字体指针对象LPD3DXFONT D3DFont = NULL;// Direct3D设备指针对象LPDIRECT3DDEVICE9 D3DDevice = NULL;public:// 构造函数Gui(LPDIRECT3DDEVICE9 device, LPD3DXFONT font, float x, float y, float w, float h, const wchar_t* t, const wchar_t* f);// 渲染函数void render(RECT rect);// 析构函数~Gui();};

我们的大致思路就是在构造函数中,完成四边形顶点缓冲区对象的构建,然后提供一个render函数用于渲染四边形,并赋予纹理贴图。那么对应的“gui.cpp”文件内如如下:

#include "gui.h"// 定义GUI类:构造函数
Gui::Gui(LPDIRECT3DDEVICE9 device, LPD3DXFONT font, float x, float y, float w, float h, const wchar_t* t, const wchar_t* f) {// 类的属性赋值posX = x, posY = y;width = w, height = h;txt = t, bg = f;D3DFont = font;D3DDevice = device;// 根据位置和尺寸计算四边形四个顶点坐标float xLeft = posX;float xRight = posX + width;float yUp = posY;float yDown = posY + height;// 定义四边形顶点数组D3D_GUI_DATA_VERTEX vertexArray[] ={{ xLeft,  yDown, 0.0f, 1.0f, 0.0f, 1.0f },{ xLeft,  yUp,   0.0f, 1.0f, 0.0f, 0.0f },{ xRight, yUp,  0.0f, 1.0f, 1.0f, 0.0f },{ xLeft,  yDown, 0.0f, 1.0f, 0.0f, 1.0f },{ xRight, yUp,   0.0f, 1.0f, 1.0f, 0.0f },{ xRight, yDown, 0.0f, 1.0f, 1.0f, 1.0f },};// 创建顶点缓冲区对象D3DDevice->CreateVertexBuffer(sizeof(vertexArray), 0, D3D_GUI_FVF_VERTEX, D3DPOOL_DEFAULT, &buffer, NULL);// 填充顶点缓冲区对象void* ptr;buffer->Lock(0, sizeof(vertexArray), (void**)&ptr, 0);memcpy(ptr, vertexArray, sizeof(vertexArray));buffer->Unlock();// 创建纹理对象//D3DXCreateTextureFromFile(D3DDevice, bg, &texture);D3DXCreateTextureFromFileEx(D3DDevice,                    // 调入是IDirect3DDevice9对象指针bg,                           // 文件名D3DX_DEFAULT,             // 图像的宽,默认D3DX_DEFAULT即可D3DX_DEFAULT,                // 图像的长,默认D3DX_DEFAULT即可D3DX_DEFAULT,                // 图片的图层,默认D3DX_DEFAULT即可0,                          // 设定这个纹理的使用方法,这个参数可以是0,D3DFMT_UNKNOWN,               // 纹理的格式,使用D3DFMT_UNKNOWN即可D3DPOOL_MANAGED,          // 一般使用D3DPOOL_MANAGEDD3DX_FILTER_TRIANGLE,     // 图像像素的过滤方式,D3DX_FILTER_TRIANGLED3DX_FILTER_TRIANGLE,       // MIP的像素过滤方式,D3DX_FILTER_TRIANGLED3DCOLOR_RGBA(0, 0, 0, 255),// 透明色,D3DCOLOR_RGBA(0,0,0,255)即可NULL,                      // 记录载入图片信息,使用NULL即可NULL,                        // 记录调色板信息,使用NULL即可&texture);                    // 用来储存载入图片的纹理对象实例
};// 定义GUI类:渲染函数
void Gui::render(RECT rect) {// 设置纹理D3DDevice->SetTexture(0, texture);// 绘制四边形D3DDevice->SetStreamSource(0, buffer, 0, sizeof(D3D_GUI_DATA_VERTEX));D3DDevice->SetFVF(D3D_GUI_FVF_VERTEX);D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);// 绘制文本if (wcslen(txt) > 0) {rect.top = posY + 5;D3DFont->DrawText(0,                              // ID3DXSprite对象的指针txt,                         // 指向要绘制的字符串-1,                             // 字符串中的字符数&rect,                           // 文本绘制矩形RECTDT_CENTER,                     // 文本对齐格式D3DCOLOR_XRGB(255, 255, 255)); // 文本的颜色}
};// 定义GUI类:析构函数
Gui::~Gui() {buffer->Release();buffer = NULL;texture->Release();texture = NULL;
};

代码中基本上都是我们学过的内容。在创建纹理对象的时候,我们使用了D3DXCreateTextureFromFileEx来加载一个不是正方形的图片,其实使用D3DXCreateTextureFromFile也没有多大的问题。这个问题不在深究了。完成Gui的封装之后,我们就开始主源码文件main.cpp的,首先是全局变量的声明:

// 引入头文件
#include "main.h"
#include "gui.h"// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 字体指针对象
LPD3DXFONT D3DFont = NULL;// 定义GUI控件数组
Gui* bg, * btn1, * btn2;

因为我们已经封装好了,因此只需要通过Gui来定义三个类即可。接下来就是initScene函数,内容如下:

// 创建字体对象
D3DXCreateFont(D3DDevice,               // DirectX设备对象24,                       // 字体的高度,以逻辑单位为单位0,                      // 字体的宽度,以逻辑单位为单位1,                      // 字体的权重,是否加粗1,                      // 文本Mipmap级别false,                 // 文本是否倾斜DEFAULT_CHARSET,       // 字符集OUT_DEFAULT_PRECIS,       // 指定Windows尝试将所需的字号和特征与实际字体匹配DEFAULT_QUALITY,      // 指定Windows字体与实际字体匹配0,                     // pitch和family索引L"微软雅黑",             // 字体类型&D3DFont);               // 字体对象// 初始化背景图bg = new Gui(D3DDevice, D3DFont, 0, 0, 800, 600, L"", L"bg.jpg");// 初始化按钮1btn1 = new Gui(D3DDevice, D3DFont, 265, 280, 270, 35, L"开始游戏", L"btn.jpg");// 初始化按钮2btn2 = new Gui(D3DDevice, D3DFont, 265, 325, 270, 35, L"结束游戏", L"btn.jpg");

创建字体对象的大致参数也有注释,大家了解一些就可以了。因为我们不需要投影和光照,所以去掉了initProjection函数和initLight函数。接下来就是renderScene函数,代码如下:

// 定义一个矩形(窗口大小)
RECT rect = { 0, 0, WINDOW_WIDTH , WINDOW_HEIGHT };// 渲染三个GUI
bg->render(rect);
btn1->render(rect);
btn2->render(rect);

经过我的封装后,Gui的使用就非常简单了。运行程序,效果如下:

最后,我们来介绍一下如何判断鼠标点击事件,也就是我们的update方法:

// 鼠标点击事件
if (type != 3) return;// 是否单击"开始游戏"按钮
if (mx > btn1->posX && mx < btn1->posX + btn1->width && my > btn1->posY && my < btn1->posY + btn1->height) {MessageBox(NULL, L"开始游戏", L"提示", 0);
}// 是否单击"结束游戏"按钮
if (mx > btn2->posX && mx < btn2->posX + btn2->width && my > btn2->posY && my < btn2->posY + btn2->height) {MessageBox(NULL, L"结束游戏", L"提示", 0);
}

其实这个算法很简单,就是判断鼠标的位置是否落在按钮四边形的区域内。运行之后,点击按钮就会有提示框显示出来了。最后一个重点要说的,就是窗口尺寸和客户区尺寸的问题。虽然我们设置窗体的大小是800*600,但是我们真正使用的区域(客户区)并不是这么大。因为每个Windows的窗体都存在标题和边框。因此,为了能让鼠标位置判断准确,我们需要将我们的客户区调整为800*600。这样,我们窗体实际尺寸会大于800*600了。

当我们调用CreateWindowEx()函数时,我们分别使用800和600来设置窗口的尺寸。但是窗口尺寸与客户区的尺寸不同,客户区是指窗口中不包含边界的那部分。窗口的尺寸是从边框的外部开始计算,而客户端的尺寸是从边框的内部开始计算。当渲染时,我们只会在窗口的客户端区域上进行绘制,因此,了解客户端区域的尺寸十分重要。当我们使用D3D进行绘制时,需要指定要生成的图像的尺寸,如果客户端区域的尺寸与图像的尺寸不符,那么图像会被拉伸或压缩以适应客户端区域的尺寸。与其先设置窗口尺寸然后确定客户端尺寸,还不如先确定客户端尺寸再确定合适的窗口尺寸。为此,我们需要在创建窗口之前使用AdjustWindowRect()函数。这个函数的功能十分简单,它所做的就是获取客户端区域的尺寸和位置,然后计算必要的窗口的尺寸和位置。

// 指定客户端区域的大小为 WINDOW_WIDTH 和 WINDOW_HEIGHT
// 然后重新计算窗口的大小 w 和 h
RECT clientSize = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
AdjustWindowRect(&clientSize, WS_OVERLAPPEDWINDOW, FALSE);
int w = clientSize.right - clientSize.left;
int h = clientSize.bottom - clientSize.top;

然后再我们的CreateWindow和MoveWindow方法中,使用新的w和h就行了。

本课程的所有代码案例下载地址:

workspace.zip

备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

第五章 DirectX 光照,材质和纹理(下)相关推荐

  1. HTML5 2D游戏引擎研发系列 第五章

    HTML5 2D游戏引擎研发系列 第五章 <Canvas技术篇-画布技术-纹理集复杂动画> 作者:HTML5游戏开发者社区-白泽 转载请注明出处:http://html5gamedev.o ...

  2. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引...

    Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引 原文:Introduction to 3 ...

  3. 交互式计算机图形学总结:第五章 光照和明暗绘制

    第五章 光照和明暗绘制 光照的一些概念 –从光源照射到物体的光传递了反射(Reflective)光[包括漫反射(Diffuse)和镜面(Specular)反射],透明(Transparent)光和吸收 ...

  4. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引

    代码工程地址: https://github.com/jiabaodan/Direct12BookReadingNotes 学习目标 回顾视景坐标系变换的数学算法: 熟悉第一人称摄像机的功能: 实现第 ...

  5. 【转】光照、材质、纹理的关系

    光照.材质.纹理的关系? 它们的关系的确不好描述. 1.材质与纹理: 1)         纹理?纹理是这个物体具体的表现形状.通过贴图反映,你可以认为它是物体的本来颜色或图案(或外在属性).所以,光 ...

  6. OpenGL蓝宝书源码学习(十)第五章——纹理的应用、Mip贴图、各项异性过滤和纹理压缩基础

    一.纹理应用 1.纹理坐标 我们是通过为每个顶点指定一个纹理坐标而直接在几何图形上进行纹理贴图的.纹理坐标要么是指定为着色器的一个属性,要么通过算法计算出来.纹理贴图中的纹理单元是作为一个更加抽象的纹 ...

  7. 【转载】【《Real-Time Rendering 3rd》 提炼总结】(四) 第五章 · 图形渲染与视觉外观 The Visual Appearance

    本文由@浅墨_毛星云 出品,转载请注明出处.   文章链接: http://blog.csdn.net/poem_qianmo/article/details/72857602 这篇文章将总结和提炼& ...

  8. 【《Real-Time Rendering 3rd》 提炼总结】(四) 第五章 · 图形渲染与视觉外观 The Visual Appearance

    本文由@浅墨_毛星云 出品,转载请注明出处.   文章链接: http://blog.csdn.net/poem_qianmo/article/details/72857602 这篇文章将总结和提炼& ...

  9. 【《Real-Time Rendering 3rd》 提炼总结】(四) 第五章 · 图形渲染与视觉外观

    本文由 @浅墨_毛星云  出品,转载请注明出处.    文章链接:http://blog.csdn.net/poem_qianmo/article/details/72857602 这篇文章将总结和提 ...

  10. 【 Real Time Rendering 3rd 提炼总结】 四 第五章 图形渲染与视觉外观 The Visu

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 本文由@ ...

最新文章

  1. /etc/inittab
  2. 解决微信二次分享失败--后面被加上from=singlemessageisappinstalled=0的解决方案
  3. java反射机制(三)---java的反射和代理实现IOC模式 模拟spring
  4. hdu-1796 How many integers can you find---容斥定理
  5. Effective Java之避免创建不必要的对象
  6. react之使用js创建虚拟DOM
  7. 【Java】《Java编程的逻辑》第4章 类的继承 笔记+感悟分享
  8. vue - 组件的创建
  9. LinearLayout和RelativeLayout
  10. 不输3000元旗舰!红米Note 7 Pro堆料有点狠
  11. html flex 的高度,html – 使flexbox行成为最短子元素的高度?
  12. c语言用switch字母判断星期几,c语言程序,输入年月日,判断这一天是这一年的第几天,同时判断这一天是星期几。(用switch语句)...
  13. C++模板函数/类示例
  14. 用Asp.Net c#写的采集小例子
  15. linux apache php.ini,Linux Apache的.htaccess控制php.ini 与MIME类型
  16. 三星研发的另一种解读
  17. 杭电OJ 1094(C++)
  18. Java前后端分离项目跨域问题
  19. Mysql读写分离的四种方案
  20. qstring 字符串查找_怎样用QT查找字符串并标记要查找的内容

热门文章

  1. matlab opencv模板匹配算法,OpenCV模板匹配函数matchTemplate详解
  2. 智能车的转弯部分_教训:渣土车“两米生死线”千万别跨!记者带你亲身体验...
  3. [译]露天矿山道路设计指南:第一章
  4. BP神经网络(BPNN)——参数确定
  5. Linux CPU负载率的计算方式
  6. 消防信号总线原理_消防二总线的构成以及工作原理?
  7. SDRAM控制器仿真
  8. 9针15针VGA接口引脚定义
  9. 学习笔记(3)-重叠社区发现Copra算法
  10. 日志分析的大救星 — — 集算器