第13章 起飞前的冲刺——Direct3D绘制核心机制
13.1 顶点缓存的崛起
13.1.1 从基础概念说起
在Windows游戏编程的世界里,3D图形的绘制是核心技能之一。当我们谈论Direct3D时,顶点缓存(Vertex Buffer)是一个无法绕开的重要概念。简单来说,顶点缓存就是显卡内存中的一块特殊区域,专门用来存储3D模型的顶点数据。这些数据包括顶点的位置坐标、颜色信息、纹理坐标、法线向量等所有定义模型外观和特性的属性。
为什么我们需要顶点缓存呢?这得从计算机图形渲染的基本原理说起。在早期的图形编程中,开发者通常直接将顶点数据存储在系统内存中,每次绘制时都需要将数据从系统内存传输到显卡内存。这个过程就像每次做饭都要去超市买食材一样低效。而顶点缓存的出现,相当于在厨房里建了一个储藏室,把常用的食材提前准备好,需要时直接取用,大大提高了效率。
在商业游戏开发中,顶点缓存的应用无处不在。以《古墓丽影》系列为例,游戏中的主角劳拉由成千上万个三角形组成,每个三角形由三个顶点定义。如果每次绘制都要从系统内存传输顶点数据,游戏的帧率将低到无法接受。通过使用顶点缓存,开发者可以将劳拉的模型数据一次性上传到显卡内存,在游戏过程中反复使用,实现了流畅的视觉效果。
13.1.2 顶点缓存核心技术解析
顶点缓存的工作原理基于现代图形处理器的架构设计。GPU被优化为并行处理大量相似的计算任务,顶点处理正是其中之一。当我们将顶点数据存储在顶点缓存中时,GPU可以高效地访问这些数据,并进行坐标变换、光照计算等操作。
顶点缓存与系统内存中的顶点数组有本质区别。系统内存由CPU管理,访问速度相对较慢,且不适合GPU直接访问。而顶点缓存位于显卡内存中,GPU可以高速访问,减少了数据总线上的传输开销。这种设计符合“数据局部性”原则——将处理器需要的数据放在它最容易访问的地方。
在Direct3D中,顶点缓存通过IDirect3DVertexBuffer9接口表示。这个接口封装了顶点缓存的所有操作,包括创建、锁定、解锁和绘制。理解这个接口的使用方法是掌握Direct3D顶点渲染的关键。
13.1.3 顶点缓存构建第一步:数据结构设计
在创建顶点缓存之前,我们必须先设计顶点的数据结构。这就像建房子前要先设计蓝图一样。顶点结构定义了每个顶点包含哪些信息,这些信息将决定模型的外观和特性。
在商业游戏开发中,顶点结构的设计需要平衡视觉效果和性能开销。一个复杂的顶点结构可能包含位置、颜色、多个纹理坐标、切线、副切线等属性,可以支持高级渲染效果,但也会增加内存占用和传输时间。一个简单的顶点结构只包含必要的信息,性能更高,但可能无法实现某些视觉效果。
让我们看一个典型的顶点结构设计,这个设计平衡了功能性和性能:
// 定义自定义顶点格式
struct CustomVertex
{
// 顶点位置(X, Y, Z坐标)
float x, y, z;
// 漫反射颜色(RGBA格式)
DWORD color;
// 纹理坐标(U, V坐标)
float u, v;
// 构造函数,便于初始化
CustomVertex(float posX, float posY, float posZ,
DWORD vertexColor, float texU, float texV)
{
x = posX;
y = posY;
z = posZ;
color = vertexColor;
u = texU;
v = texV;
}
// 默认构造函数
CustomVertex()
{
x = y = z = 0.0f;
color = 0xFFFFFFFF; // 默认白色
u = v = 0.0f;
}
};
// 定义顶点格式描述
#define CUSTOM_FVF (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
在这个结构中,我们定义了顶点的三个基本属性:位置、颜色和纹理坐标。D3DFVF_XYZ表示顶点包含未经变换的3D坐标,D3DFVF_DIFFUSE表示顶点包含漫反射颜色,D3DFVF_TEX1表示顶点包含一组纹理坐标。
在商业实践中,顶点结构的设计会根据目标硬件和游戏需求进行调整。例如,面向低端设备的手机游戏可能使用更简单的顶点结构,而面向高端PC的AAA游戏可能使用包含更多属性的复杂顶点结构,以支持法线贴图、视差映射等高级效果。
13.1.4 顶点缓存构建第二步:创建与初始化
创建顶点缓存是Direct3D渲染流程中的关键步骤。这个过程类似于在显卡内存中分配一块专门用于存储顶点数据的区域。正确的创建和初始化可以确保渲染效率,而错误的方法则可能导致性能问题甚至渲染失败。
在Direct3D 9中,我们使用CreateVertexBuffer方法来创建顶点缓存。这个方法需要几个关键参数:缓存大小、使用方式、顶点格式和内存池。让我们通过一个完整的示例来理解这个过程:
// Direct3D顶点缓存管理类
class VertexBufferManager
{
private:
// Direct3D设备指针
LPDIRECT3DDEVICE9 direct3DDevice;
// 顶点缓存指针
LPDIRECT3DVERTEXBUFFER9 vertexBuffer;
// 顶点数量
int vertexCount;
public:
// 构造函数
VertexBufferManager(LPDIRECT3DDEVICE9 d3dDevice)
{
direct3DDevice = d3DDevice;
vertexBuffer = NULL;
vertexCount = 0;
}
// 析构函数,释放资源
~VertexBufferManager()
{
ReleaseResources();
}
// 创建顶点缓存
bool CreateVertexBuffer(int numVertices)
{
// 计算顶点缓存大小(每个顶点的大小乘以顶点数量)
int bufferSize = sizeof(CustomVertex) * numVertices;
// 创建顶点缓存
HRESULT result = direct3DDevice->CreateVertexBuffer(
bufferSize, // 缓存大小
D3DUSAGE_WRITEONLY, // 使用方式:只写
CUSTOM_FVF, // 顶点格式
D3DPOOL_MANAGED, // 内存池:由Direct3D管理
&vertexBuffer, // 返回的顶点缓存指针
NULL); // 保留参数,必须为NULL
if (FAILED(result))
{
MessageBox(NULL,
"创建顶点缓存失败",
"错误",
MB_OK | MB_ICONERROR);
return false;
}
vertexCount = numVertices;
return true;
}
// 填充顶点数据
bool FillVertexData(const CustomVertex* vertices)
{
if (vertexBuffer == NULL || vertices == NULL)
{
return false;
}
// 锁定顶点缓存,获取指向内存的指针
void* bufferPointer;
HRESULT result = vertexBuffer->Lock(
0, // 偏移量(从开头开始)
0, // 锁定大小(0表示锁定全部)
&bufferPointer, // 返回的内存指针
0); // 锁定标志
if (FAILED(result))
{
MessageBox(NULL,
"锁定顶点缓存失败",
"错误",
MB_OK | MB_ICONERROR);
return false;
}
// 复制顶点数据到顶点缓存
memcpy(bufferPointer,
vertices,
sizeof(CustomVertex) * vertexCount);
// 解锁顶点缓存
vertexBuffer->Unlock();
return true;
}
// 释放资源
void ReleaseResources()
{
if (vertexBuffer != NULL)
{
vertexBuffer->Release();
vertexBuffer = NULL;
}
vertexCount = 0;
}
// 获取顶点缓存指针
LPDIRECT3DVERTEXBUFFER9 GetVertexBuffer() const
{
return vertexBuffer;
}
// 获取顶点数量
int GetVertexCount() const
{
return vertexCount;
}
};
在这个示例中,我们创建了一个专门管理顶点缓存的类。CreateVertexBuffer方法负责创建顶点缓存,它使用D3DUSAGE_WRITEONLY标志,表示这个缓存只用于写入数据,这允许驱动程序进行优化。D3DPOOL_MANAGED表示缓存由Direct3D管理,当设备丢失时,Direct3D会自动恢复缓存内容。
FillVertexData方法展示了如何将顶点数据从系统内存复制到顶点缓存。这个过程涉及锁定缓存、复制数据和解锁缓存三个步骤。锁定操作获取指向缓存内存的指针,解锁操作通知Direct3D数据复制完成。
在商业游戏引擎如Unreal Engine或Unity中,顶点缓存的创建和管理更加复杂。它们通常包含内存池管理系统,可以重用已分配的顶点缓存,减少内存分配开销。此外,这些引擎还实现了各种优化策略,如根据硬件的特性选择不同的内存池类型。
13.1.5 顶点缓存构建第三步:数据访问与更新
顶点缓存创建并填充数据后,我们可能需要更新其中的数据。例如,在实现角色动画、水面波动或布料模拟时,顶点位置会随时间变化。Direct3D提供了灵活的机制来访问和更新顶点缓存数据。
访问顶点缓存数据有两种主要方式:只读访问和读写访问。只读访问用于绘制操作,效率最高;读写访问用于更新数据,但性能较低。在商业游戏开发中,开发者会根据具体需求选择合适的访问方式。
让我们通过一个示例来展示如何动态更新顶点缓存数据:
// 动态顶点更新管理器
class DynamicVertexUpdater
{
private:
// 顶点缓存指针
LPDIRECT3DVERTEXBUFFER9 dynamicVertexBuffer;
// 顶点数据
std::vector<CustomVertex> vertexData;
public:
// 构造函数
DynamicVertexUpdater()
{
dynamicVertexBuffer = NULL;
}
// 初始化动态顶点缓存
bool InitializeDynamicBuffer(LPDIRECT3DDEVICE9 d3dDevice,
int initialVertexCount)
{
// 创建动态顶点缓存
int bufferSize = sizeof(CustomVertex) * initialVertexCount;
HRESULT result = d3dDevice->CreateVertexBuffer(
bufferSize,
D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, // 动态缓存,只写
CUSTOM_FVF,
D3DPOOL_DEFAULT, // 默认内存池
&dynamicVertexBuffer,
NULL);
if (FAILED(result))
{
return false;
}
// 初始化顶点数据
vertexData.resize(initialVertexCount);
for (int i = 0; i < initialVertexCount; i++)
{
vertexData[i] = CustomVertex();
}
return true;
}
// 更新单个顶点
void UpdateVertex(int index,
float newX, float newY, float newZ,
DWORD newColor, float newU, float newV)
{
if (index < 0 || index >= vertexData.size())
{
return;
}
vertexData[index].x = newX;
vertexData[index].y = newY;
vertexData[index].z = newZ;
vertexData[index].color = newColor;
vertexData[index].u = newU;
vertexData[index].v = newV;
}
// 应用所有更新到GPU
bool ApplyUpdates()
{
if (dynamicVertexBuffer == NULL)
{
return false;
}
// 使用DISCARD标志锁定缓存,提高性能
void* bufferPointer;
HRESULT result = dynamicVertexBuffer->Lock(
0, // 偏移量
0, // 大小(0表示全部)
&bufferPointer, // 返回的指针
D3DLOCK_DISCARD); // 丢弃原有内容
if (FAILED(result))
{
return false;
}
// 复制更新后的数据
memcpy(bufferPointer,
vertexData.data(),
sizeof(CustomVertex) * vertexData.size());
// 解锁缓存
dynamicVertexBuffer->Unlock();
return true;
}
// 部分更新(只更新部分顶点)
bool PartialUpdate(int startIndex, int count)
{
if (dynamicVertexBuffer == NULL ||
startIndex < 0 ||
startIndex + count > vertexData.size())
{
return false;
}
// 计算偏移量和大小
int offset = startIndex * sizeof(CustomVertex);
int size = count * sizeof(CustomVertex);
// 使用NOOVERWRITE标志锁定,允许在绘制时更新
void* bufferPointer;
HRESULT result = dynamicVertexBuffer->Lock(
offset, // 偏移量
size, // 大小
&bufferPointer, // 返回的指针
D3DLOCK_NOOVERWRITE); // 不覆盖正在使用的数据
if (FAILED(result))
{
return false;
}
// 复制部分数据
memcpy(bufferPointer,
&vertexData[startIndex],
size);
// 解锁缓存
dynamicVertexBuffer->Unlock();
return true;
}
// 获取顶点缓存
LPDIRECT3DVERTEXBUFFER9 GetBuffer() const
{
return dynamicVertexBuffer;
}
// 获取顶点数量
int GetVertexCount() const
{
return vertexData.size();
}
// 释放资源
void ReleaseResources()
{
if (dynamicVertexBuffer != NULL)
{
dynamicVertexBuffer->Release();
dynamicVertexBuffer = NULL;
}
vertexData.clear();
}
};
这个示例展示了动态顶点缓存的使用。与静态缓存不同,动态缓存使用D3DUSAGE_DYNAMIC标志创建,表示其内容会频繁更新。在更新数据时,我们使用不同的锁定标志来优化性能:
在商业游戏中,动态顶点缓存有广泛的应用。例如,在《魔兽世界》中,角色的披风和头发使用动态顶点缓存实现随风飘动的效果。在竞速游戏中,赛车的旗帜使用动态顶点缓存模拟飘动效果。粒子系统也大量使用动态顶点缓存,因为粒子的位置和属性每帧都在变化。
13.1.6 顶点缓存构建第四步:图形绘制
创建并填充顶点缓存后,最后一步是使用这些顶点数据绘制图形。绘制过程是Direct3D渲染管线的核心,它将顶点数据转换为屏幕上的像素。理解绘制流程对于优化游戏性能至关重要。
Direct3D支持多种图元类型,包括点、线和三角形。三角形是3D图形的基础,因为任何复杂表面都可以用三角形网格近似表示。绘制三角形时,我们需要指定绘制方式和顶点顺序。
让我们通过一个完整的示例来展示如何使用顶点缓存绘制图形:
// 简单三角形绘制器
class TriangleRenderer
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 顶点缓存管理器
VertexBufferManager vertexBufferManager;
public:
// 构造函数
TriangleRenderer(LPDIRECT3DDEVICE9 d3dDevice)
: vertexBufferManager(d3dDevice)
{
direct3DDevice = d3dDevice;
}
// 初始化三角形数据
bool InitializeTriangle()
{
// 创建3个顶点的缓存
if (!vertexBufferManager.CreateVertexBuffer(3))
{
return false;
}
// 定义三角形顶点数据
CustomVertex triangleVertices[3] =
{
// 顶点0:位置(-1.0, -1.0, 0.0),红色,纹理坐标(0.0, 1.0)
CustomVertex(–1.0f, –1.0f, 0.0f,
D3DCOLOR_XRGB(255, 0, 0),
0.0f, 1.0f),
// 顶点1:位置(0.0, 1.0, 0.0),绿色,纹理坐标(0.5, 0.0)
CustomVertex(0.0f, 1.0f, 0.0f,
D3DCOLOR_XRGB(0, 255, 0),
0.5f, 0.0f),
// 顶点2:位置(1.0, -1.0, 0.0),蓝色,纹理坐标(1.0, 1.0)
CustomVertex(1.0f, –1.0f, 0.0f,
D3DCOLOR_XRGB(0, 0, 255),
1.0f, 1.0f)
};
// 填充顶点数据
return vertexBufferManager.FillVertexData(triangleVertices);
}
// 绘制三角形
void RenderTriangle()
{
if (direct3DDevice == NULL)
{
return;
}
// 设置顶点格式
direct3DDevice->SetFVF(CUSTOM_FVF);
// 设置顶点流源(指定使用哪个顶点缓存)
direct3DDevice->SetStreamSource(
0, // 流编号
vertexBufferManager.GetVertexBuffer(), // 顶点缓存
0, // 偏移量
sizeof(CustomVertex)); // 每个顶点的大小
// 绘制图元
direct3DDevice->DrawPrimitive(
D3DPT_TRIANGLELIST, // 图元类型:三角形列表
0, // 起始顶点索引
1); // 图元数量(1个三角形)
}
// 绘制多个三角形
void RenderMultipleTriangles(int triangleCount)
{
if (direct3DDevice == NULL || triangleCount <= 0)
{
return;
}
// 创建足够多的顶点
int vertexCount = triangleCount * 3;
if (!vertexBufferManager.CreateVertexBuffer(vertexCount))
{
return;
}
// 生成顶点数据
std::vector<CustomVertex> vertices(vertexCount);
for (int i = 0; i < triangleCount; i++)
{
float offsetX = (i % 10) * 0.5f – 2.5f;
float offsetY = (i / 10) * 0.5f – 2.5f;
// 每个三角形的三个顶点
int baseIndex = i * 3;
// 顶点0
vertices[baseIndex] = CustomVertex(
offsetX – 0.2f, offsetY – 0.2f, 0.0f,
D3DCOLOR_XRGB(255, 0, 0), 0.0f, 1.0f);
// 顶点1
vertices[baseIndex + 1] = CustomVertex(
offsetX, offsetY + 0.2f, 0.0f,
D3DCOLOR_XRGB(0, 255, 0), 0.5f, 0.0f);
// 顶点2
vertices[baseIndex + 2] = CustomVertex(
offsetX + 0.2f, offsetY – 0.2f, 0.0f,
D3DCOLOR_XRGB(0, 0, 255), 1.0f, 1.0f);
}
// 填充数据并绘制
vertexBufferManager.FillVertexData(vertices.data());
// 设置渲染状态
direct3DDevice->SetFVF(CUSTOM_FVF);
direct3DDevice->SetStreamSource(0,
vertexBufferManager.GetVertexBuffer(),
0,
sizeof(CustomVertex));
// 绘制所有三角形
direct3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST,
0,
triangleCount);
}
// 使用不同图元类型绘制
void RenderWithDifferentPrimitives()
{
if (direct3DDevice == NULL)
{
return;
}
// 绘制点列表
CustomVertex points[4] =
{
CustomVertex(–1.0f, 1.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 0.0f),
CustomVertex(–0.5f, 1.0f, 0.0f, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 0.0f),
CustomVertex(0.0f, 1.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 0.0f),
CustomVertex(0.5f, 1.0f, 0.0f, D3DCOLOR_XRGB(255, 255, 0), 0.0f, 0.0f)
};
VertexBufferManager pointBufferManager(direct3DDevice);
pointBufferManager.CreateVertexBuffer(4);
pointBufferManager.FillVertexData(points);
direct3DDevice->SetFVF(CUSTOM_FVF);
direct3DDevice->SetStreamSource(0,
pointBufferManager.GetVertexBuffer(),
0,
sizeof(CustomVertex));
direct3DDevice->DrawPrimitive(D3DPT_POINTLIST, 0, 4);
// 绘制线列表
CustomVertex lines[4] =
{
CustomVertex(–1.0f, 0.5f, 0.0f, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 0.0f),
CustomVertex(–0.5f, 0.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 0.0f),
CustomVertex(0.0f, 0.5f, 0.0f, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 0.0f),
CustomVertex(0.5f, 0.0f, 0.0f, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 0.0f)
};
VertexBufferManager lineBufferManager(direct3DDevice);
lineBufferManager.CreateVertexBuffer(4);
lineBufferManager.FillVertexData(lines);
direct3DDevice->SetStreamSource(0,
lineBufferManager.GetVertexBuffer(),
0,
sizeof(CustomVertex));
direct3DDevice->DrawPrimitive(D3DPT_LINELIST, 0, 2); // 2条线
// 绘制三角形带(连续三角形)
CustomVertex triangleStrip[4] =
{
CustomVertex(–1.0f, –0.5f, 0.0f, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 0.0f),
CustomVertex(–0.5f, –1.0f, 0.0f, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 0.0f),
CustomVertex(0.0f, –0.5f, 0.0f, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 0.0f),
CustomVertex(0.5f, –1.0f, 0.0f, D3DCOLOR_XRGB(255, 255, 0), 0.0f, 0.0f)
};
VertexBufferManager stripBufferManager(direct3DDevice);
stripBufferManager.CreateVertexBuffer(4);
stripBufferManager.FillVertexData(triangleStrip);
direct3DDevice->SetStreamSource(0,
stripBufferManager.GetVertexBuffer(),
0,
sizeof(CustomVertex));
direct3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // 2个三角形
}
};
这个示例展示了如何使用顶点缓存绘制不同类型的图元。关键步骤包括:
在商业游戏开发中,绘制调用(Draw Call)是性能优化的关键指标。每次调用DrawPrimitive都会产生一定的CPU开销,因此减少绘制调用数量是常见的优化手段。游戏开发者通常使用以下策略:
例如,在《刺客信条》系列中,城市中的大量行人和士兵使用实例化技术渲染,显著减少了绘制调用数量。在《我的世界》中,相邻的方块面被合并为更大的图元,减少了顶点和绘制调用数量。
13.1.7 综合示例:简单3D场景渲染
现在,让我们将这些概念整合到一个完整的示例中,展示如何使用顶点缓存渲染一个简单的3D场景:
// 简单3D场景渲染器
class Simple3DSceneRenderer
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 顶点缓存数组
std::vector<LPDIRECT3DVERTEXBUFFER9> vertexBuffers;
// 索引缓存数组
std::vector<LPDIRECT3DINDEXBUFFER9> indexBuffers;
// 模型数据
struct ModelData
{
int vertexCount;
int primitiveCount;
D3DPRIMITIVETYPE primitiveType;
};
std::vector<ModelData> modelDataList;
public:
// 构造函数
Simple3DSceneRenderer(LPDIRECT3DDEVICE9 d3dDevice)
{
direct3DDevice = d3dDevice;
}
// 析构函数
~Simple3DSceneRenderer()
{
ReleaseAllResources();
}
// 创建立方体模型
bool CreateCubeModel()
{
// 立方体有8个顶点,但为了每个面有独立的法线,我们需要24个顶点
const int cubeVertexCount = 24;
CustomVertex cubeVertices[cubeVertexCount];
// 定义立方体的8个顶点位置
float cubeSize = 1.0f;
DWORD faceColors[6] =
{
D3DCOLOR_XRGB(255, 0, 0), // 前面 – 红色
D3DCOLOR_XRGB(0, 255, 0), // 后面 – 绿色
D3DCOLOR_XRGB(0, 0, 255), // 左面 – 蓝色
D3DCOLOR_XRGB(255, 255, 0), // 右面 – 黄色
D3DCOLOR_XRGB(255, 0, 255), // 顶面 – 紫色
D3DCOLOR_XRGB(0, 255, 255) // 底面 – 青色
};
// 前面(4个顶点)
cubeVertices[0] = CustomVertex(–cubeSize, –cubeSize, –cubeSize,
faceColors[0], 0.0f, 1.0f);
cubeVertices[1] = CustomVertex(–cubeSize, cubeSize, –cubeSize,
faceColors[0], 0.0f, 0.0f);
cubeVertices[2] = CustomVertex(cubeSize, –cubeSize, –cubeSize,
faceColors[0], 1.0f, 1.0f);
cubeVertices[3] = CustomVertex(cubeSize, cubeSize, –cubeSize,
faceColors[0], 1.0f, 0.0f);
// 后面(4个顶点)
cubeVertices[4] = CustomVertex(cubeSize, –cubeSize, cubeSize,
faceColors[1], 0.0f, 1.0f);
cubeVertices[5] = CustomVertex(cubeSize, cubeSize, cubeSize,
faceColors[1], 0.0f, 0.0f);
cubeVertices[6] = CustomVertex(–cubeSize, –cubeSize, cubeSize,
faceColors[1], 1.0f, 1.0f);
cubeVertices[7] = CustomVertex(–cubeSize, cubeSize, cubeSize,
faceColors[1], 1.0f, 0.0f);
// 左面(4个顶点)
cubeVertices[8] = CustomVertex(–cubeSize, –cubeSize, cubeSize,
faceColors[2], 0.0f, 1.0f);
cubeVertices[9] = CustomVertex(–cubeSize, cubeSize, cubeSize,
faceColors[2], 0.0f, 0.0f);
cubeVertices[10] = CustomVertex(–cubeSize, –cubeSize, –cubeSize,
faceColors[2], 1.0f, 1.0f);
cubeVertices[11] = CustomVertex(–cubeSize, cubeSize, –cubeSize,
faceColors[2], 1.0f, 0.0f);
// 右面(4个顶点)
cubeVertices[12] = CustomVertex(cubeSize, –cubeSize, –cubeSize,
faceColors[3], 0.0f, 1.0f);
cubeVertices[13] = CustomVertex(cubeSize, cubeSize, –cubeSize,
faceColors[3], 0.0f, 0.0f);
cubeVertices[14] = CustomVertex(cubeSize, –cubeSize, cubeSize,
faceColors[3], 1.0f, 1.0f);
cubeVertices[15] = CustomVertex(cubeSize, cubeSize, cubeSize,
faceColors[3], 1.0f, 0.0f);
// 顶面(4个顶点)
cubeVertices[16] = CustomVertex(–cubeSize, cubeSize, –cubeSize,
faceColors[4], 0.0f, 1.0f);
cubeVertices[17] = CustomVertex(–cubeSize, cubeSize, cubeSize,
faceColors[4], 0.0f, 0.0f);
cubeVertices[18] = CustomVertex(cubeSize, cubeSize, –cubeSize,
faceColors[4], 1.0f, 1.0f);
cubeVertices[19] = CustomVertex(cubeSize, cubeSize, cubeSize,
faceColors[4], 1.0f, 0.0f);
// 底面(4个顶点)
cubeVertices[20] = CustomVertex(–cubeSize, –cubeSize, cubeSize,
faceColors[5], 0.0f, 1.0f);
cubeVertices[21] = CustomVertex(–cubeSize, –cubeSize, –cubeSize,
faceColors[5], 0.0f, 0.0f);
cubeVertices[22] = CustomVertex(cubeSize, –cubeSize, cubeSize,
faceColors[5], 1.0f, 1.0f);
cubeVertices[23] = CustomVertex(cubeSize, –cubeSize, –cubeSize,
faceColors[5], 1.0f, 0.0f);
// 创建顶点缓存
LPDIRECT3DVERTEXBUFFER9 cubeVertexBuffer = NULL;
int bufferSize = sizeof(CustomVertex) * cubeVertexCount;
HRESULT result = direct3DDevice->CreateVertexBuffer(
bufferSize,
D3DUSAGE_WRITEONLY,
CUSTOM_FVF,
D3DPOOL_MANAGED,
&cubeVertexBuffer,
NULL);
if (FAILED(result))
{
return false;
}
// 填充顶点数据
void* bufferPointer;
cubeVertexBuffer->Lock(0, 0, &bufferPointer, 0);
memcpy(bufferPointer, cubeVertices, bufferSize);
cubeVertexBuffer->Unlock();
// 保存顶点缓存
vertexBuffers.push_back(cubeVertexBuffer);
// 保存模型数据(立方体有12个三角形,使用三角形列表)
ModelData cubeData;
cubeData.vertexCount = cubeVertexCount;
cubeData.primitiveCount = 12; // 6个面 * 2个三角形/面
cubeData.primitiveType = D3DPT_TRIANGLESTRIP; // 使用三角形带更高效
modelDataList.push_back(cubeData);
return true;
}
// 创建金字塔模型
bool CreatePyramidModel()
{
// 金字塔有5个顶点(4个底面顶点 + 1个顶点)
const int pyramidVertexCount = 5;
CustomVertex pyramidVertices[pyramidVertexCount];
float pyramidHeight = 2.0f;
float pyramidBase = 1.5f;
DWORD pyramidColor = D3DCOLOR_XRGB(255, 128, 0); // 橙色
// 底面四个顶点
pyramidVertices[0] = CustomVertex(–pyramidBase, 0.0f, –pyramidBase,
pyramidColor, 0.0f, 1.0f);
pyramidVertices[1] = CustomVertex(–pyramidBase, 0.0f, pyramidBase,
pyramidColor, 0.0f, 0.0f);
pyramidVertices[2] = CustomVertex(pyramidBase, 0.0f, –pyramidBase,
pyramidColor, 1.0f, 1.0f);
pyramidVertices[3] = CustomVertex(pyramidBase, 0.0f, pyramidBase,
pyramidColor, 1.0f, 0.0f);
// 顶点
pyramidVertices[4] = CustomVertex(0.0f, pyramidHeight, 0.0f,
pyramidColor, 0.5f, 0.5f);
// 创建顶点缓存
LPDIRECT3DVERTEXBUFFER9 pyramidVertexBuffer = NULL;
int bufferSize = sizeof(CustomVertex) * pyramidVertexCount;
HRESULT result = direct3DDevice->CreateVertexBuffer(
bufferSize,
D3DUSAGE_WRITEONLY,
CUSTOM_FVF,
D3DPOOL_MANAGED,
&pyramidVertexBuffer,
NULL);
if (FAILED(result))
{
return false;
}
// 填充数据
void* bufferPointer;
pyramidVertexBuffer->Lock(0, 0, &bufferPointer, 0);
memcpy(bufferPointer, pyramidVertices, bufferSize);
pyramidVertexBuffer->Unlock();
// 保存顶点缓存
vertexBuffers.push_back(pyramidVertexBuffer);
// 保存模型数据(金字塔有6个三角形:4个侧面 + 2个底面三角形)
ModelData pyramidData;
pyramidData.vertexCount = pyramidVertexCount;
pyramidData.primitiveCount = 6;
pyramidData.primitiveType = D3DPT_TRIANGLELIST; // 使用三角形列表
modelDataList.push_back(pyramidData);
return true;
}
// 渲染所有模型
void RenderAllModels()
{
if (direct3DDevice == NULL)
{
return;
}
// 设置渲染状态
direct3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE); // 禁用光照
direct3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); // 禁用背面剔除
// 设置顶点格式
direct3DDevice->SetFVF(CUSTOM_FVF);
// 渲染所有模型
for (size_t i = 0; i < vertexBuffers.size(); i++)
{
if (i < modelDataList.size())
{
// 设置顶点流
direct3DDevice->SetStreamSource(0,
vertexBuffers[i],
0,
sizeof(CustomVertex));
// 绘制模型
direct3DDevice->DrawPrimitive(
modelDataList[i].primitiveType,
0,
modelDataList[i].primitiveCount);
}
}
}
// 释放所有资源
void ReleaseAllResources()
{
for (size_t i = 0; i < vertexBuffers.size(); i++)
{
if (vertexBuffers[i] != NULL)
{
vertexBuffers[i]->Release();
vertexBuffers[i] = NULL;
}
}
vertexBuffers.clear();
for (size_t i = 0; i < indexBuffers.size(); i++)
{
if (indexBuffers[i] != NULL)
{
indexBuffers[i]->Release();
indexBuffers[i] = NULL;
}
}
indexBuffers.clear();
modelDataList.clear();
}
};
这个综合示例展示了如何使用顶点缓存创建和渲染3D模型。我们创建了两个基本模型:立方体和金字塔。每个模型使用单独的顶点缓存存储顶点数据。
在实际商业游戏开发中,3D模型的创建和渲染更加复杂。游戏引擎通常使用模型文件(如.obj、.fbx格式)存储模型数据,包括顶点位置、法线、纹理坐标等信息。在运行时,引擎读取这些文件,创建顶点缓存,并管理模型的渲染。
13.2 索引缓存——顶点缓存的默契搭档
13.2.1 问题的提出与解决思路
当我们使用顶点缓存绘制复杂模型时,会遇到一个效率问题:许多顶点被多个三角形共享。以立方体为例,虽然只有8个顶点,但如果为每个三角形独立存储顶点数据,需要24个顶点(6个面 × 每个面2个三角形 × 每个三角形3个顶点)。这意味着大量重复数据,浪费了宝贵的内存和带宽。
索引缓存(Index Buffer)正是为了解决这个问题而设计的。索引缓存存储的是顶点索引,而不是顶点数据本身。每个索引指向顶点缓存中的一个顶点。通过组合这些索引,我们可以定义三角形而无需重复顶点数据。
让我们通过一个简单的比喻来理解顶点缓存和索引缓存的关系:想象顶点缓存是一个演员列表,每个演员有独特的技能和外观。索引缓存则是一个剧本,指定哪些演员在哪些场景中出现。通过使用剧本(索引缓存),我们可以让同一个演员(顶点)出现在多个场景(三角形)中,而不需要为每个场景雇佣新演员。
在商业游戏开发中,索引缓存的应用非常广泛。以《最终幻想》系列为例,游戏中的角色模型可能有数千个三角形,但通过索引缓存,顶点数量可以大大减少,从而节省内存,提高渲染速度。
13.2.2 索引缓存的使用策略
索引缓存的使用遵循一种简单而高效的策略:首先创建一个包含所有不重复顶点的顶点缓存,然后创建一个索引缓存,其中包含定义三角形所需的顶点索引。绘制时,Direct3D使用索引来确定哪些顶点组成每个三角形。
这种策略的优势显而易见:
让我们通过一个完整的示例来展示如何使用索引缓存:
// 索引缓存管理器
class IndexBufferManager
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 索引缓存指针
LPDIRECT3DINDEXBUFFER9 indexBuffer;
// 索引数量
int indexCount;
// 索引大小(16位或32位)
int indexSize;
public:
// 构造函数
IndexBufferManager(LPDIRECT3DDEVICE9 d3dDevice)
{
direct3DDevice = d3dDevice;
indexBuffer = NULL;
indexCount = 0;
indexSize = sizeof(WORD); // 默认使用16位索引
}
// 析构函数
~IndexBufferManager()
{
ReleaseResources();
}
// 创建索引缓存(16位索引)
bool CreateIndexBuffer16(int numIndices)
{
indexSize = sizeof(WORD);
return CreateIndexBufferInternal(numIndices, D3DFMT_INDEX16);
}
// 创建索引缓存(32位索引)
bool CreateIndexBuffer32(int numIndices)
{
indexSize = sizeof(DWORD);
return CreateIndexBufferInternal(numIndices, D3DFMT_INDEX32);
}
// 内部创建方法
bool CreateIndexBufferInternal(int numIndices, D3DFORMAT format)
{
// 计算缓存大小
int bufferSize = indexSize * numIndices;
// 创建索引缓存
HRESULT result = direct3DDevice->CreateIndexBuffer(
bufferSize, // 缓存大小
D3DUSAGE_WRITEONLY, // 使用方式:只写
format, // 索引格式
D3DPOOL_MANAGED, // 内存池:由Direct3D管理
&indexBuffer, // 返回的索引缓存指针
NULL); // 保留参数
if (FAILED(result))
{
MessageBox(NULL,
"创建索引缓存失败",
"错误",
MB_OK | MB_ICONERROR);
return false;
}
indexCount = numIndices;
return true;
}
// 填充索引数据(16位)
bool FillIndexData16(const WORD* indices)
{
if (indexBuffer == NULL || indices == NULL)
{
return false;
}
// 锁定索引缓存
void* bufferPointer;
HRESULT result = indexBuffer->Lock(
0, // 偏移量
0, // 大小(0表示全部)
&bufferPointer, // 返回的指针
0); // 锁定标志
if (FAILED(result))
{
return false;
}
// 复制数据
memcpy(bufferPointer,
indices,
sizeof(WORD) * indexCount);
// 解锁缓存
indexBuffer->Unlock();
return true;
}
// 填充索引数据(32位)
bool FillIndexData32(const DWORD* indices)
{
if (indexBuffer == NULL || indices == NULL)
{
return false;
}
// 锁定索引缓存
void* bufferPointer;
HRESULT result = indexBuffer->Lock(
0, // 偏移量
0, // 大小(0表示全部)
&bufferPointer, // 返回的指针
0); // 锁定标志
if (FAILED(result))
{
return false;
}
// 复制数据
memcpy(bufferPointer,
indices,
sizeof(DWORD) * indexCount);
// 解锁缓存
indexBuffer->Unlock();
return true;
}
// 获取索引缓存
LPDIRECT3DINDEXBUFFER9 GetIndexBuffer() const
{
return indexBuffer;
}
// 获取索引数量
int GetIndexCount() const
{
return indexCount;
}
// 获取索引大小
int GetIndexSize() const
{
return indexSize;
}
// 释放资源
void ReleaseResources()
{
if (indexBuffer != NULL)
{
indexBuffer->Release();
indexBuffer = NULL;
}
indexCount = 0;
}
};
// 使用顶点缓存和索引缓存绘制正方形的示例
class SquareRendererWithIndexBuffer
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 顶点缓存管理器
VertexBufferManager vertexBufferManager;
// 索引缓存管理器
IndexBufferManager indexBufferManager;
public:
// 构造函数
SquareRendererWithIndexBuffer(LPDIRECT3DDEVICE9 d3dDevice)
: vertexBufferManager(d3dDevice), indexBufferManager(d3dDevice)
{
direct3DDevice = d3dDevice;
}
// 初始化正方形数据
bool InitializeSquare()
{
// 正方形的4个顶点
const int squareVertexCount = 4;
CustomVertex squareVertices[squareVertexCount] =
{
// 左下角
CustomVertex(–1.0f, –1.0f, 0.0f,
D3DCOLOR_XRGB(255, 0, 0),
0.0f, 1.0f),
// 左上角
CustomVertex(–1.0f, 1.0f, 0.0f,
D3DCOLOR_XRGB(0, 255, 0),
0.0f, 0.0f),
// 右下角
CustomVertex(1.0f, –1.0f, 0.0f,
D3DCOLOR_XRGB(0, 0, 255),
1.0f, 1.0f),
// 右上角
CustomVertex(1.0f, 1.0f, 0.0f,
D3DCOLOR_XRGB(255, 255, 0),
1.0f, 0.0f)
};
// 创建和填充顶点缓存
if (!vertexBufferManager.CreateVertexBuffer(squareVertexCount))
{
return false;
}
if (!vertexBufferManager.FillVertexData(squareVertices))
{
return false;
}
// 正方形的索引(2个三角形,6个索引)
const int squareIndexCount = 6;
WORD squareIndices[squareIndexCount] =
{
0, 1, 2, // 第一个三角形:左下、左上、右下
1, 3, 2 // 第二个三角形:左上、右上、右下
};
// 创建和填充索引缓存
if (!indexBufferManager.CreateIndexBuffer16(squareIndexCount))
{
return false;
}
if (!indexBufferManager.FillIndexData16(squareIndices))
{
return false;
}
return true;
}
// 绘制正方形
void RenderSquare()
{
if (direct3DDevice == NULL)
{
return;
}
// 设置顶点格式
direct3DDevice->SetFVF(CUSTOM_FVF);
// 设置顶点流源
direct3DDevice->SetStreamSource(
0,
vertexBufferManager.GetVertexBuffer(),
0,
sizeof(CustomVertex));
// 设置索引缓存
direct3DDevice->SetIndices(indexBufferManager.GetIndexBuffer());
// 使用索引绘制图元
direct3DDevice->DrawIndexedPrimitive(
D3DPT_TRIANGLELIST, // 图元类型
0, // 基础顶点索引
0, // 最小顶点索引
vertexBufferManager.GetVertexCount(), // 顶点数量
0, // 起始索引
2); // 图元数量(2个三角形)
}
};
这个示例展示了如何使用顶点缓存和索引缓存绘制一个正方形。正方形由两个三角形组成,使用4个顶点和6个索引。相比为每个三角形独立存储6个顶点,这种方法节省了2个顶点的存储空间。
在更复杂的模型中,节省的空间更加显著。例如,一个包含10,000个三角形的模型,如果平均每个顶点被3个三角形共享,使用索引缓存可以将顶点数量从30,000减少到大约10,000,节省了约66%的内存。
13.2.3 顶点缓存与索引缓存的协同工作
顶点缓存和索引缓存的关系可以比作字典和索引的关系。顶点缓存像字典,包含所有不重复的条目(顶点);索引缓存像索引,告诉我们在哪里找到组成每个单词(三角形)的字母(顶点)。
这种协同工作模式有几个重要优势:
在商业游戏引擎中,顶点缓存和索引缓存的协同优化是渲染性能的关键。例如,在《孤岛危机》系列中,开发者使用复杂的算法来重新排列顶点和索引,以最大化GPU缓存的效率。这种优化虽然增加了预处理时间,但显著提高了运行时性能。
13.2.4 双剑合璧:完整使用流程
要充分利用顶点缓存和索引缓存的优势,我们需要遵循一个系统的使用流程。这个流程包括四个关键步骤:设计、创建、填充和绘制。
让我们通过一个更复杂的示例来展示这个完整流程:
// 复杂模型渲染器(使用顶点缓存和索引缓存)
class ComplexModelRenderer
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 顶点缓存
LPDIRECT3DVERTEXBUFFER9 complexVertexBuffer;
// 索引缓存
LPDIRECT3DINDEXBUFFER9 complexIndexBuffer;
// 顶点数量
int vertexCount;
// 索引数量
int indexCount;
// 图元数量
int primitiveCount;
public:
// 构造函数
ComplexModelRenderer(LPDIRECT3DDEVICE9 d3dDevice)
{
direct3DDevice = d3dDevice;
complexVertexBuffer = NULL;
complexIndexBuffer = NULL;
vertexCount = 0;
indexCount = 0;
primitiveCount = 0;
}
// 析构函数
~ComplexModelRenderer()
{
ReleaseResources();
}
// 步骤1:设计模型数据结构
struct ComplexVertex
{
float x, y, z; // 位置
DWORD color; // 颜色
float nx, ny, nz; // 法线(用于光照计算)
float u, v; // 纹理坐标
ComplexVertex(float posX, float posY, float posZ,
DWORD vertexColor,
float normalX, float normalY, float normalZ,
float texU, float texV)
{
x = posX; y = posY; z = posZ;
color = vertexColor;
nx = normalX; ny = normalY; nz = normalZ;
u = texU; v = texV;
}
ComplexVertex()
{
x = y = z = 0.0f;
color = 0xFFFFFFFF;
nx = ny = 0.0f; nz = 1.0f;
u = v = 0.0f;
}
};
// 复杂顶点格式
#define COMPLEX_FVF (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_NORMAL | D3DFVF_TEX1)
// 步骤2:创建顶点缓存和索引缓存
bool CreateModelBuffers(int numVertices, int numIndices)
{
vertexCount = numVertices;
indexCount = numIndices;
// 计算缓存大小
int vertexBufferSize = sizeof(ComplexVertex) * vertexCount;
int indexBufferSize = sizeof(WORD) * indexCount;
// 创建顶点缓存
HRESULT result = direct3DDevice->CreateVertexBuffer(
vertexBufferSize,
D3DUSAGE_WRITEONLY,
COMPLEX_FVF,
D3DPOOL_MANAGED,
&complexVertexBuffer,
NULL);
if (FAILED(result))
{
MessageBox(NULL,
"创建顶点缓存失败",
"错误",
MB_OK | MB_ICONERROR);
return false;
}
// 创建索引缓存
result = direct3DDevice->CreateIndexBuffer(
indexBufferSize,
D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&complexIndexBuffer,
NULL);
if (FAILED(result))
{
MessageBox(NULL,
"创建索引缓存失败",
"错误",
MB_OK | MB_ICONERROR);
// 清理已创建的顶点缓存
if (complexVertexBuffer != NULL)
{
complexVertexBuffer->Release();
complexVertexBuffer = NULL;
}
return false;
}
return true;
}
// 步骤3:填充顶点和索引数据
bool FillModelData(const ComplexVertex* vertices, const WORD* indices)
{
if (complexVertexBuffer == NULL || complexIndexBuffer == NULL)
{
return false;
}
// 填充顶点缓存
void* vertexBufferPointer;
HRESULT result = complexVertexBuffer->Lock(
0, 0, &vertexBufferPointer, 0);
if (FAILED(result))
{
return false;
}
memcpy(vertexBufferPointer,
vertices,
sizeof(ComplexVertex) * vertexCount);
complexVertexBuffer->Unlock();
// 填充索引缓存
void* indexBufferPointer;
result = complexIndexBuffer->Lock(0, 0, &indexBufferPointer, 0);
if (FAILED(result))
{
return false;
}
memcpy(indexBufferPointer, indices, sizeof(WORD) * indexCount);
complexIndexBuffer->Unlock();
// 计算图元数量(假设所有图元都是三角形)
primitiveCount = indexCount / 3;
return true;
}
// 创建圆柱体模型
bool CreateCylinderModel(float radius, float height, int segments)
{
if (segments < 3)
{
segments = 3; // 至少需要3个段来形成圆柱体
}
// 计算顶点和索引数量
// 圆柱体有:顶部中心点 + 底部中心点 + 顶部圆环顶点 + 底部圆环顶点
vertexCount = 2 + segments * 2;
// 索引数量:顶部三角形扇 + 底部三角形扇 + 侧面四边形(每个四边形2个三角形)
indexCount = segments * 3 * 2 + segments * 6;
// 创建缓存
if (!CreateModelBuffers(vertexCount, indexCount))
{
return false;
}
// 生成顶点数据
std::vector<ComplexVertex> vertices(vertexCount);
std::vector<WORD> indices(indexCount);
// 颜色定义
DWORD topColor = D3DCOLOR_XRGB(255, 0, 0); // 红色
DWORD bottomColor = D3DCOLOR_XRGB(0, 0, 255); // 蓝色
DWORD sideColor = D3DCOLOR_XRGB(0, 255, 0); // 绿色
// 顶部中心点(索引0)
vertices[0] = ComplexVertex(0.0f, height / 2, 0.0f,
topColor,
0.0f, 1.0f, 0.0f, // 法线向上
0.5f, 0.5f); // 纹理中心
// 底部中心点(索引1)
vertices[1] = ComplexVertex(0.0f, –height / 2, 0.0f,
bottomColor,
0.0f, –1.0f, 0.0f, // 法线向下
0.5f, 0.5f);
// 生成顶部和底部的圆环顶点
float angleStep = 2.0f * 3.1415926f / segments;
for (int i = 0; i < segments; i++)
{
float angle = i * angleStep;
float x = cosf(angle) * radius;
float z = sinf(angle) * radius;
// 顶部圆环顶点(索引2 + i)
vertices[2 + i] = ComplexVertex(x, height / 2, z,
topColor,
0.0f, 1.0f, 0.0f, // 法线向上
(cosf(angle) + 1.0f) / 2.0f,
(sinf(angle) + 1.0f) / 2.0f);
// 底部圆环顶点(索引2 + segments + i)
vertices[2 + segments + i] = ComplexVertex(x, –height / 2, z,
sideColor,
x, 0.0f, z, // 法线向外
float(i) / segments,
0.0f);
// 侧面顶部圆环顶点(索引2 + segments * 2 + i)
if (2 + segments * 2 + i < vertexCount)
{
vertices[2 + segments * 2 + i] = ComplexVertex(x, height / 2, z,
sideColor,
x, 0.0f, z, // 法线向外
float(i) / segments,
1.0f);
}
}
// 生成索引数据
// 顶部三角形扇
int currentIndex = 0;
for (int i = 0; i < segments; i++)
{
indices[currentIndex++] = 0; // 中心点
indices[currentIndex++] = 2 + i; // 当前点
indices[currentIndex++] = 2 + ((i + 1) % segments); // 下一个点
}
// 底部三角形扇(注意顶点顺序要相反,以确保法线方向正确)
for (int i = 0; i < segments; i++)
{
indices[currentIndex++] = 1; // 中心点
indices[currentIndex++] = 2 + segments + ((i + 1) % segments); // 下一个点
indices[currentIndex++] = 2 + segments + i; // 当前点
}
// 侧面四边形(每个四边形2个三角形)
for (int i = 0; i < segments; i++)
{
int nextI = (i + 1) % segments;
// 第一个三角形
indices[currentIndex++] = 2 + segments + i; // 底部当前点
indices[currentIndex++] = 2 + segments + nextI; // 底部下一个点
indices[currentIndex++] = 2 + segments * 2 + i; // 顶部当前点
// 第二个三角形
indices[currentIndex++] = 2 + segments + nextI; // 底部下一个点
indices[currentIndex++] = 2 + segments * 2 + nextI; // 顶部下一个点
indices[currentIndex++] = 2 + segments * 2 + i; // 顶部当前点
}
// 填充数据
return FillModelData(vertices.data(), indices.data());
}
// 步骤4:绘制模型
void RenderModel()
{
if (direct3DDevice == NULL ||
complexVertexBuffer == NULL ||
complexIndexBuffer == NULL)
{
return;
}
// 设置顶点格式
direct3DDevice->SetFVF(COMPLEX_FVF);
// 设置顶点流源
direct3DDevice->SetStreamSource(0,
complexVertexBuffer,
0,
sizeof(ComplexVertex));
// 设置索引缓存
direct3DDevice->SetIndices(complexIndexBuffer);
// 绘制索引图元
direct3DDevice->DrawIndexedPrimitive(
D3DPT_TRIANGLELIST, // 图元类型
0, // 基础顶点索引
0, // 最小顶点索引
vertexCount, // 顶点数量
0, // 起始索引
primitiveCount); // 图元数量
}
// 释放资源
void ReleaseResources()
{
if (complexVertexBuffer != NULL)
{
complexVertexBuffer->Release();
complexVertexBuffer = NULL;
}
if (complexIndexBuffer != NULL)
{
complexIndexBuffer->Release();
complexIndexBuffer = NULL;
}
vertexCount = 0;
indexCount = 0;
primitiveCount = 0;
}
};
这个复杂的示例展示了如何使用顶点缓存和索引缓存创建和渲染一个圆柱体模型。圆柱体是一个相对复杂的几何形状,由顶部、底部和侧面组成。通过合理设计顶点和索引,我们可以用较少的内存表示这个模型。
在商业游戏开发中,这种技术被用于创建各种复杂的模型。例如,在《魔兽世界》中,树木、岩石和建筑等环境元素使用类似的技巧创建。通过共享顶点和使用索引,游戏可以在有限的内存中存储大量复杂的几何形状。
13.2.5 商业级示例:地形渲染系统
地形渲染是游戏开发中的一个重要领域,它展示了顶点缓存和索引缓存的强大能力。让我们通过一个简化的地形渲染系统来了解这些技术在实际游戏中的应用:
// 简化地形渲染系统
class TerrainRenderer
{
private:
// Direct3D设备
LPDIRECT3DDEVICE9 direct3DDevice;
// 地形顶点缓存
LPDIRECT3DVERTEXBUFFER9 terrainVertexBuffer;
// 地形索引缓存
LPDIRECT3DINDEXBUFFER9 terrainIndexBuffer;
// 地形参数
int terrainWidth; // 地形宽度(顶点数)
int terrainHeight; // 地形高度(顶点数)
float cellSpacing; // 单元格间距
// 地形高度图
std::vector<float> heightMap;
public:
// 构造函数
TerrainRenderer(LPDIRECT3DDEVICE9 d3dDevice)
{
direct3DDevice = d3dDevice;
terrainVertexBuffer = NULL;
terrainIndexBuffer = NULL;
terrainWidth = 0;
terrainHeight = 0;
cellSpacing = 1.0f;
}
// 析构函数
~TerrainRenderer()
{
ReleaseTerrainResources();
}
// 地形顶点结构
struct TerrainVertex
{
float x, y, z; // 位置
DWORD color; // 颜色(基于高度)
float nx, ny, nz; // 法线
float u, v; // 纹理坐标
TerrainVertex()
{
x = y = z = 0.0f;
color = 0xFFFFFFFF;
nx = ny = 0.0f; nz = 1.0f;
u = v = 0.0f;
}
};
// 地形顶点格式
#define TERRAIN_FVF (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_NORMAL | D3DFVF_TEX1)
// 生成随机高度图
void GenerateRandomHeightMap(int width, int height, float maxHeight)
{
terrainWidth = width;
terrainHeight = height;
// 重置高度图
heightMap.resize(width * height);
// 使用简单的随机高度
srand(static_cast<unsigned>(time(NULL)));
for (int z = 0; z < height; z++)
{
for (int x = 0; x < width; x++)
{
// 生成随机高度(0到maxHeight之间)
float heightValue =
(static_cast<float>(rand()) / RAND_MAX) * maxHeight;
heightMap[z * width + x] = heightValue;
}
}
// 平滑高度图(简单的平均滤波)
SmoothHeightMap(2);
}
// 平滑高度图
void SmoothHeightMap(int iterations)
{
if (heightMap.empty())
{
return;
}
std::vector<float> smoothedHeightMap = heightMap;
for (int iter = 0; iter < iterations; iter++)
{
for (int z = 1; z < terrainHeight – 1; z++)
{
for (int x = 1; x < terrainWidth – 1; x++)
{
// 计算3×3区域的平均高度
float sum = 0.0f;
for (int dz = –1; dz <= 1; dz++)
{
for (int dx = –1; dx <= 1; dx++)
{
sum += heightMap[(z + dz) * terrainWidth + (x + dx)];
}
}
smoothedHeightMap[z * terrainWidth + x] = sum / 9.0f;
}
}
// 更新高度图
heightMap = smoothedHeightMap;
}
}
// 计算顶点法线
void CalculateNormals(std::vector<TerrainVertex>& vertices)
{
// 临时法线累加器
std::vector<D3DXVECTOR3> tempNormals(vertices.size(), D3DXVECTOR3(0, 0, 0));
// 遍历所有三角形(每个四边形有2个三角形)
for (int z = 0; z < terrainHeight – 1; z++)
{
for (int x = 0; x < terrainWidth – 1; x++)
{
// 获取当前四边形的四个顶点索引
int v0 = z * terrainWidth + x; // 左上
int v1 = z * terrainWidth + (x + 1); // 右上
int v2 = (z + 1) * terrainWidth + x; // 左下
int v3 = (z + 1) * terrainWidth + (x + 1); // 右下
// 第一个三角形(v0, v1, v2)
D3DXVECTOR3 p0(vertices[v0].x, vertices[v0].y, vertices[v0].z);
D3DXVECTOR3 p1(vertices[v1].x, vertices[v1].y, vertices[v1].z);
D3DXVECTOR3 p2(vertices[v2].x, vertices[v2].y, vertices[v2].z);
D3DXVECTOR3 edge1 = p1 – p0;
D3DXVECTOR3 edge2 = p2 – p0;
D3DXVECTOR3 normal;
D3DXVec3Cross(&normal, &edge1, &edge2);
D3DXVec3Normalize(&normal, &normal);
// 累加法线到三个顶点
tempNormals[v0] += normal;
tempNormals[v1] += normal;
tempNormals[v2] += normal;
// 第二个三角形(v1, v3, v2)
D3DXVECTOR3 p3(vertices[v3].x, vertices[v3].y, vertices[v3].z);
edge1 = p3 – p1;
edge2 = p2 – p1;
D3DXVec3Cross(&normal, &edge1, &edge2);
D3DXVec3Normalize(&normal, &normal);
// 累加法线到三个顶点
tempNormals[v1] += normal;
tempNormals[v3] += normal;
tempNormals[v2] += normal;
}
}
// 归一化所有法线
for (size_t i = 0; i < vertices.size(); i++)
{
D3DXVec3Normalize(&tempNormals[i], &tempNormals[i]);
vertices[i].nx = tempNormals[i].x;
vertices[i].ny = tempNormals[i].y;
vertices[i].nz = tempNormals[i].z;
}
}
// 根据高度获取颜色
DWORD GetColorByHeight(float height, float maxHeight)
{
// 简单的高度颜色映射
if (height < maxHeight * 0.3f)
{
// 低处:深绿色
return D3DCOLOR_XRGB(0, 100, 0);
}
else if (height < maxHeight * 0.6f)
{
// 中处:绿色
return D3DCOLOR_XRGB(0, 150, 0);
}
else if (height < maxHeight * 0.8f)
{
// 高处:棕色
return D3DCOLOR_XRGB(139, 69, 19);
}
else
{
// 最高处:白色(雪)
return D3DCOLOR_XRGB(255, 255, 255);
}
}
// 创建地形
bool CreateTerrain(int width, int height, float spacing, float maxHeight)
{
terrainWidth = width;
terrainHeight = height;
cellSpacing = spacing;
// 生成高度图
GenerateRandomHeightMap(width, height, maxHeight);
// 创建顶点
int vertexCount = width * height;
std::vector<TerrainVertex> vertices(vertexCount);
// 生成顶点数据
for (int z = 0; z < height; z++)
{
for (int x = 0; x < width; x++)
{
int index = z * width + x;
vertices[index].x = x * spacing – (width * spacing) / 2.0f;
vertices[index].z = z * spacing – (height * spacing) / 2.0f;
vertices[index].y = heightMap[index];
// 根据高度设置颜色
vertices[index].color = GetColorByHeight(heightMap[index], maxHeight);
// 纹理坐标(简单映射)
vertices[index].u = static_cast<float>(x) / (width – 1);
vertices[index].v = static_cast<float>(z) / (height – 1);
}
}
// 计算法线
CalculateNormals(vertices);
// 创建索引
int indexCount = (width – 1) * (height – 1) * 6; // 每个四边形2个三角形,每个三角形3个索引
std::vector<WORD> indices(indexCount);
int currentIndex = 0;
for (int z = 0; z < height – 1; z++)
{
for (int x = 0; x < width – 1; x++)
{
// 当前四边形的四个顶点
WORD v0 = static_cast<WORD>(z * width + x); // 左上
WORD v1 = static_cast<WORD>(z * width + (x + 1)); // 右上
WORD v2 = static_cast<WORD>((z + 1) * width + x); // 左下
WORD v3 = static_cast<WORD>((z + 1) * width + (x + 1)); // 右下
// 第一个三角形(左上,右上,左下)
indices[currentIndex++] = v0;
indices[currentIndex++] = v1;
indices[currentIndex++] = v2;
// 第二个三角形(右上,右下,左下)
indices[currentIndex++] = v1;
indices[currentIndex++] = v3;
indices[currentIndex++] = v2;
}
}
// 创建顶点缓存
int vertexBufferSize = sizeof(TerrainVertex) * vertexCount;
HRESULT result = direct3DDevice->CreateVertexBuffer(
vertexBufferSize,
D3DUSAGE_WRITEONLY,
TERRAIN_FVF,
D3DPOOL_MANAGED,
&terrainVertexBuffer,
NULL);
if (FAILED(result))
{
return false;
}
// 填充顶点缓存
void* vertexBufferPointer;
terrainVertexBuffer->Lock(0, 0, &vertexBufferPointer, 0);
memcpy(vertexBufferPointer, vertices.data(), vertexBufferSize);
terrainVertexBuffer->Unlock();
// 创建索引缓存
int indexBufferSize = sizeof(WORD) * indexCount;
result = direct3DDevice->CreateIndexBuffer(
indexBufferSize,
D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&terrainIndexBuffer,
NULL);
if (FAILED(result))
{
terrainVertexBuffer->Release();
terrainVertexBuffer = NULL;
return false;
}
// 填充索引缓存
void* indexBufferPointer;
terrainIndexBuffer->Lock(0, 0, &indexBufferPointer, 0);
memcpy(indexBufferPointer, indices.data(), indexBufferSize);
terrainIndexBuffer->Unlock();
return true;
}
// 渲染地形
void RenderTerrain()
{
if (direct3DDevice == NULL ||
terrainVertexBuffer == NULL ||
terrainIndexBuffer == NULL)
{
return;
}
// 启用光照
direct3DDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
// 设置材质
D3DMATERIAL9 terrainMaterial;
ZeroMemory(&terrainMaterial, sizeof(D3DMATERIAL9));
terrainMaterial.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
terrainMaterial.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f);
direct3DDevice->SetMaterial(&terrainMaterial);
// 设置简单的方向光
D3DLIGHT9 light;
ZeroMemory(&light, sizeof(D3DLIGHT9));
light.Type = D3DLIGHT_DIRECTIONAL;
light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
light.Ambient = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
light.Direction = D3DXVECTOR3(0.0f, –1.0f, 0.5f);
direct3DDevice->SetLight(0, &light);
direct3DDevice->LightEnable(0, TRUE);
// 设置顶点格式
direct3DDevice->SetFVF(TERRAIN_FVF);
// 设置顶点流
direct3DDevice->SetStreamSource(0,
terrainVertexBuffer,
0,
sizeof(TerrainVertex));
// 设置索引缓存
direct3DDevice->SetIndices(terrainIndexBuffer);
// 计算图元数量
int primitiveCount = (terrainWidth – 1) * (terrainHeight – 1) * 2;
// 绘制地形
direct3DDevice->DrawIndexedPrimitive(
D3DPT_TRIANGLELIST,
0,
0,
terrainWidth * terrainHeight,
0,
primitiveCount);
}
// 释放地形资源
void ReleaseTerrainResources()
{
if (terrainVertexBuffer != NULL)
{
terrainVertexBuffer->Release();
terrainVertexBuffer = NULL;
}
if (terrainIndexBuffer != NULL)
{
terrainIndexBuffer->Release();
terrainIndexBuffer = NULL;
}
terrainWidth = 0;
terrainHeight = 0;
cellSpacing = 1.0f;
heightMap.clear();
}
// 获取地形信息
void GetTerrainInfo(int& width, int& height, int& vertexCount, int& triangleCount)
{
width = terrainWidth;
height = terrainHeight;
vertexCount = terrainWidth * terrainHeight;
triangleCount = (terrainWidth – 1) * (terrainHeight – 1) * 2;
}
};
这个地形渲染系统展示了顶点缓存和索引缓存在实际游戏开发中的应用。地形是游戏中常见的元素,无论是《上古卷轴》中的广阔荒野,还是《我的世界》中的方块世界,都需要高效的地形渲染技术。
该系统的关键特性包括:
在商业游戏中,地形渲染系统更加复杂。例如,《微软模拟飞行》使用真实的地理数据生成地形,支持从地面到太空的无缝渲染。《荒野大镖客:救赎2》使用多层次的地形细节系统,根据观察距离调整地形复杂度。
13.3 本章总结与思考
通过本章的学习,我们深入探讨了Direct3D绘制基础中的两个核心概念:顶点缓存和索引缓存。这些技术是Windows游戏编程的基石,理解它们对于掌握3D图形渲染至关重要。
13.3.1 关键概念回顾
顶点缓存:存储在显卡内存中的顶点数据集合,用于高效访问和绘制。顶点缓存减少了CPU和GPU之间的数据传输,提高了渲染性能。
索引缓存:存储顶点索引的缓存,用于定义如何将顶点组合成图元。索引缓存允许顶点被多个三角形共享,减少了内存使用和数据传输。
顶点格式:定义顶点包含哪些属性(位置、颜色、法线、纹理坐标等)的结构。正确的顶点格式设计是高效渲染的关键。
绘制调用:命令GPU绘制指定图元的过程。减少绘制调用是性能优化的重要策略。
批处理:将多个物体合并到一次绘制调用中,减少CPU开销。
13.3.2 商业游戏开发实践
在商业游戏开发中,顶点缓存和索引缓存的使用已经形成了成熟的最佳实践:
内存管理:大型游戏如《赛博朋克2077》需要管理数百万个顶点。开发者使用内存池和流式加载技术,确保只有可见的顶点数据在显卡内存中。
动态更新:角色动画、布料模拟等效果需要动态更新顶点数据。游戏如《鬼泣5》使用动态顶点缓存实现流畅的角色动作。
优化策略:现代游戏引擎如Unreal Engine和Unity实现了自动的顶点缓存优化,包括顶点缓存优化、索引缓存重组和绘制调用批处理。
硬件适应性:游戏需要适应不同的硬件配置。低端设备可能使用简化的顶点格式和较少的顶点,而高端设备则使用复杂的顶点格式支持高级特效。
13.3.3 学习路径建议
对于希望深入学习Windows游戏编程的读者,建议遵循以下学习路径:
基础掌握:首先掌握本章介绍的顶点缓存和索引缓存的基本概念和使用方法。通过实现简单的3D模型(如立方体、球体、圆柱体)来巩固理解。
性能优化:学习如何分析渲染性能,识别瓶颈。掌握减少绘制调用、优化顶点格式、使用实例化等技术。
高级特性:研究更高级的Direct3D特性,如几何着色器、曲面细分、计算着色器等。这些特性在现代游戏中广泛使用。
引擎开发:尝试开发简化版的游戏引擎,集成顶点缓存管理、材质系统、光照系统等组件。
商业实践:研究开源游戏引擎的源代码,学习商业级的最佳实践。参与游戏开发社区,了解行业最新动态。
13.3.4 未来发展趋势
随着图形硬件的不断发展,顶点缓存和索引缓存技术也在不断演进:
Mesh Shader:新一代图形API(如DirectX 12 Ultimate)引入了Mesh Shader,提供了更灵活的几何处理方式,可能改变传统的顶点缓存使用模式。
光线追踪:实时光线追踪技术需要不同的几何表示和加速结构,但顶点缓存仍然是基础数据来源。
云游戏:云游戏将渲染任务转移到服务器,需要更高效的数据压缩和传输技术,顶点缓存的优化变得更加重要。
虚拟现实:VR游戏需要更高的渲染性能(90+ FPS)和更低的延迟,对顶点处理效率提出了更高要求。
13.3.5 结语
顶点缓存和索引缓存是Direct3D绘制的基础,也是Windows游戏编程的核心技能。通过本章的学习,你应该已经掌握了这些技术的基本概念、使用方法和优化策略。
记住,优秀的游戏开发者不仅是技术的使用者,更是问题的解决者。当你遇到渲染性能问题时,不要害怕深入底层,分析顶点缓存和索引缓存的使用情况。当你设计新的渲染特性时,考虑如何最有效地利用这些基础技术。
游戏开发是一场马拉松,而不是短跑。持续学习、实践和优化是取得成功的关键。从今天开始,动手实现你自己的3D渲染器,探索图形编程的无限可能。
在接下来的章节中,我们将继续深入Direct3D的其他重要主题,包括纹理映射、光照计算、阴影技术和后期处理效果。这些技术将帮助你创建更加逼真和引人入胜的游戏世界。
祝你在Windows游戏编程的旅程中取得成功!


