第13章 DirectX编程入门
DirectX 是微软公司为编写游戏和其他高性能多媒体应用程序而开发的一组高级的
Windows 低层 API,它包含了对图(图形、视频和三维动画)、声音(声效与音乐)、输入
设备和网络游戏等的支持。
本章先概述 DirectX 的基本内容,然后介绍 DirectX 的主要组件 Direct3D 和 DirectSound
的基本编程方法,并给出若干具体编程实例。
DirectX 概述
本节简单介绍 DurectX 的发展历史、版本和功能的演化、结构和模块划分。
DurectX 是 Windows 的一种多媒体 API,它在保持设备无关性的同时,可以让应用程序
直接控制多媒体设备,从而能充分利用硬件的功能,因此可获得最高的性能。
DurectX 自从推出以来,就始终处在快速发展的过程之中。它已经经历了多个版本,其
模块和功能也一直在不断地增加和完善。
DirectX 是一种基于 COM 的系统,主要由硬件抽象层 HAL 和硬件模拟层 HEL 所构成。
DurectX 被分成若干个组件模块,涵盖了多媒体应用的方方面面。而且这些组件的数目、名
称和功能,也是随着其版本的演化而有所增减、改变和加强。
历史
微软公司于 1990 年 5 月推出的 Windows 大获成功,它是一种与设备无关的具有图形
用户界面(GUI)的操作系统。后来又于 1992 年 3 月推出 Windows ,提供了对多媒体的
支持。不过 Windows 的与设备无关的设计思想,虽然使得 Windows 向程序员提供了一个独
立于硬件的软件开发优秀平台,但它不让应用程序直接操作显卡和声卡等多媒体硬件,这样
就降低了多媒体应用程序(特别是计算机游戏)的运行效率,例如限制了动画的帧率、不提
供混音功能等。所以当时的 Windows 并不是一个可以开发和运行高效多媒体程序(如 3D 游
戏)的理想环境,逼迫 PC 游戏开发商只好绕开 Windows 而仍旧开发 DOS 下的游戏。
为了改变这种难堪的局面,促使游戏界放弃 DOS 而使用 Windows,微软需要在设备无
关性与高性能之间取得某种平衡,决定为 Windows 添加一种高性能的动画机制(以及后来的
输入与音响等),因此于 1995 年推出了 Game SDK,后来改名为 DirectX。其中的 Direct 指
可以直接访问多媒体硬件;X 指它的不同组成部分,如 Direct3D、DirectSound、DirectInput
和 DirectPlay 等。
版本与功能
因为 DirectX 必须支持多媒体设备的最新技术和功能,而多媒体硬件(尤其是显卡[的 3D
性能])和技术始终处在高速发展过程中,所以 DirectX 的技术和版本也同样在不断地发展和
演变,其中发展最快的是 3D 接口部分。
下面列出 DirectX 的主要版本的发布时间、引入的新模块和特性:
——1995 年 9 月 30 日 : DirectDraw 、 DirectSound 、 DirectInput 、 DirectPlay 、
DirectSetup;
——1996 年 6 月 5 日:引入 Direct3D(次版本有 );
——1996 年 9 月 15 日:支持 MMX、为 DirectSound 补充 DirectSound3D API(次版本
有 );
——无此版本号;
——1997 年 7 月 16 日:有许多改进,如受力反馈控制器、多显示器支持、新的游戏控
制面板、用户界面的全面改进等(次版本有 和 );
——1998 年 8 月 7 日:添加新的 3D 特性(如添加 DXUT 框架)、支持 AMD 公司的
3DNow!技术、性能较 有提高。在 1999 年 2 月 3 日推出的 版中,添加了 DirectMusic
模块;
——1999 年 9 月 22 日:改进 3D 图形和声音、性能更快,为 Direct3D API 引入硬件传
送、光照和纹理压缩、以及硬件加速,引入 D3DX 工具库。使用 Creative 公司的 EAX 技
术改进 3D 声音的算法(次版本有 和 );
——2000 年 9 月 30 日:将 DirectDraw 与 Direct3D 完全集成在一起、其中 Direct3D 的
性能有了本质上的飞跃(引入顶点/像素 shader[着色引擎/光照模型]、支持硬件点精灵和三
维体纹理),将 DirectMusic 与 DirectSound 更紧密地集成在一起构成 DirectX Audio,更
新了 DirectInput 与 DirectPlay,添加了 DirectShow、抛弃了其余的 DirectX Media 模块。
2001 年 11 月 11 日推出的 版,被集成在 Windows XP 中销售,该版本添加了从
DirectShow 分离出的 DMO(DirectX Media Objects 媒体对象)组件模块。它还支持许多
新的图形特性,如像素着色引擎的 ~ 版、动态/高分辨率纹理、新 3D 扩展方法等(次
版本有 、、 和 );
——2002 年 12 月 24 日:添加许多新特性到 DirectX Graphics 和 DirectShow 中,例如
将 DirectDraw 的功能并入到 Direct3D 之中,支持顶点和像素着色引擎的新版本 和 ,
引入裁剪测试、位移贴图和多元纹理,支持 128 位浮点着色精度和根据显示器自动调整γ
值等。也添加若干新特性到 DirectInput、DirectPlay、DirectSound 和 DirectMusic 中。(次
版本有 、 和 )。
DirectX 的最后一个以数字和字母命名的版本是 2004 年 8 月 9 日推出的 ,后来又于
2004 年 10 月、2005 年的 2/4/6/8/10 月分别推出了它的多个升级版。从 2005 年 12 月推出
的 DirectX 开始,其版本的命名发生了变化:主版本号改为 DX,次版本改为发布的年月。
一般是每两个月推出一个新版本,因此也叫做 DirectX 的双月更新(bimonthly
updates)。当前最新版本为 2006 年 10 月 11 日推出的 DX October 2006。它们都包含了
(于 2002 年推出的专为.NET 框架设计的)托管(Managed)DirectX,将原来 DirectX 多
个分离的 COM 模块集成到了一个容易使用的单一 CLR 文件之中。
10——?:DirectX 不会有第 10 版了,因为微软公司已将 DirectX 的功能集成进其新一代
操作系统 Windows Vista 中,而不再将其作为一个额外的模块了。但是会有 Direct3D 10,
已于 2006 年 12 月 13 日随 Direct3D SDK 的 December 2006 版一起发布,并被嵌入到
Windows Vista 中。另外,作为学生和业余爱好者的 DirectX 替代产品,微软公司于 2004
年 3 月 24 日宣布了用于托管运行环境下计算机游戏的设计、开发和管理的框架和工具集
XNA ,并于 2006 年 12 月 11
日 推 出 了 XNA Framework
Redistributable 和 XNA Game
Studio Express 的 版,可用
于 Windows 和 Xbox 360 游戏
的开发。
结构
DirectX 是 一 种 基 于 COM
(Component Object Model,组件对象模型)的系统,它既不属于驱动程序层,也不属于应用
层。DirectX 的主要设计目标是在提供某种设备独立性的同时获取高速度,为此微软公司为
DirectX 设计了如图 13-1 所示的体系结构。其中:
HAL = Hardware Abstract
Layer ( 硬 件 抽 象 层 )
——负责检测本机的硬件功能,并以一种独立于设备的方式提供这些功能;
HEL = Hardware Emulation Layer(硬件模拟层)——负责提供 DirectX 功能中本机
硬件不支持部分的模拟实现。
但是从 版起,DirectX 就不再提供硬件模拟层 HEL 了,需要开发商、第三方或自己
来编写一个 PSD(Pluggable Software Device 可插式软件设备)连到 DirectX 提供的 HEL 接
口上,称之为参考设备。但是现在很少有这样的 PSD 可用,因此新版本的 DirectX 是严重依
赖于硬件的。
组成
DirectX 由多个(COM 组件)模块组成,而且随着版本的演化,这些模块也在不断地增
减、合并和改变。最新的 DirectX 组件的模块构成如图 13-2 所示,括号中的数字为该模块被
首次引进(或移出)的版本号。
下面是 DirectX 的主要组成模块的简单介绍:
DirectX Graphics(图形)—— 包括 DirectDraw 和 Direct3D:
DirectDraw(绘制)—— 二维图形绘制(高速位图传送/视频与 2D 动画)。
Windows
API
应用程序
Windows API 子系统
(GDI、MMAPI、MCI、...)
Windows 驱动程序接口
DirectX
HEL HAL
PC 硬件子系统
DirectX
图 13-1 DirectX 子系统
从 版起,DirectDraw 已被集成到了 Direct3D 之中。
Direct3D ( 三 维 ) —— 三 维 图 形 绘 制 。 分 成 保 留 ( retained ) 和 立 即
(immediate)两种模式(mode):保留模式是一种高层 API,建立在立即模
式之上,使用起来方便容易,但是效率不高,适合于初学者;立即模式则是一
种底层 API,使用复杂,但是其效率比保留模式高得多。大多数 DirectX 应用
程序,特别是计算机游戏,都是采用立即模式进行开发。自从 版以来,保
留模式 API 的发展停滞不前,而立即模式 API 的发展却日新月异。不论是保
留模式还是立即模式,Direct3D 都是建立在 DirectDraw 之上的。从 版起,
Direct3D 包含了 DirectDraw,它像 OpenGL 一样也具有二维图形绘制的功能。
Direct3D 的下一个版本为 Durect3D 10,为 Windows Vista 的一个有机组成部分。
D3DX(Direct3D 扩展)—— 为 Direct3D 服务的实用工具库,执行公共
的数学计算和一些复杂的任务,包含若干简化 3D 模型使用的类,如粒子
系统(particle system)。D3DX 由动态链接库(DLL)提供。
DXUT(编程框架)—— 建立在 Direct3D API 之上的样例框架,也叫通
用文件框架,可以帮助程序员减少在繁琐的平凡工作(如创建窗口、创建
设备、处理 Windows 消息和设备事件等)上所花的时间。
图 13-2 DirectX 的组成模块
DirectX Audio(音频)——包括 DirectSound 和 DirectMusic:
DirectSound(声音)—— 主要针对波形音频,底层接口。可用于开发播放和
捕捉波形音频的高性能立体与三维(DirectSound 3D)音频应用程序。在下一代
DirectX
DirectX Graphics ()
DirectDraw ()
Direct3D ()
DirectX Audio ()
DirectSound ()
DirectMusic ()
DirectSound 3D ()
DirectInput ()
DirectPlay ()
DirectSetup ()
DirectX Media ()
DirectShow ()
DirectAnimation ()
DirectX 视频加速 ()
Direct3D 保留模式 ()
DirectX Transform ()
并入()
移入平台 SDK
( )
建议用 WinSocket 和 WinFirewall API 代替 ()
并入()
移入 Windows
Media SDK ()
XInput ( )
Xbox Live (Vista)
Media Foundation (Vista)
XACT ( )
XNA
(Xbox/Vista)
DirectX Media Objects ()
分出()
并入()
Direct3D 立即模式 ()
D3DX () / DXUT()
DirectX 中,DirectSound 的功能将由 XACT(Microsoft Cross-Platform Audio
Creation Tool 微软跨平台音频生成工具)来代替。
DirectMusic(音乐)—— 主要针对 MIDI 音乐,高层接口。为基于波形、MIDI
声音或 DirectMusic 生成器所创造的动态内容之音乐和非音乐声道提供一个完
整的解决方案。
XACT(游戏音频)—— XACT(Microsoft Cross-Platform Audio Creation Tool,
微软跨平台音频生成工具),是一种多人合作的音频设计工具和相关 API,用
于游戏的动态音频开发。
DirectInput ( 输 入 ) —— 提 供 对 各 种 输 入 设 备 的 支 持 , 包 括 对 受 力 反 馈
(force-feedback)技术的全面支持。在下一代 DirectX 中,DirectInput 将被 XInput 代替。
DirectPlay(游戏)—— 提供对多人网络游戏的支持。在最新的 DirectX 版本中,微
软公司反对(deprecate)使用 DirectPlay,而建议改用 Windows Sockets 和 Windows
Firewall API 来代替。在下一代 DirectX 中,DirectPlay 可能会被 Xbox Live 代替。
DirectSetup(安装)—— 提供 DirectX 组件的一次性调用安装的一个简单 API(自
动安装 DirectX 驱动程序)。
DirectShow(展示)—— 提供多媒体(音视频)流的高质量捕捉和重放。在 版
之前,DirectShow 是 DirectX Media 的一个组成模块。到 版时,因为 DirectX SDK
抛弃了 DirectX Media,所以才将 DirectShow 集成到了 DirectX 之中。在 DirectX
的 2005 年 4 月升级版中,DirectShow 又被移出 DirectX,放入微软公司的平台 SDK
之中。
DirectX Media Objects(媒体对象 DMO)—— 基于 COM 的多媒体和数据流组件,
支持音视频的编解码器和特效。是在 DirectX 的 版时,从 DirectShow 分出的,其
功能类似于 DirectShow 的过滤器。
DirectX Media(媒体)—— 是一个用于流媒体应用的外部 API 集合。在 DirectX
之前,DirectX Media SDK 和 DirectX SDK 是两个分离的 SDK。在 版时 DirectX
Media SDK 被集成到 DirectX SDK 之中。DirectX Media 的运行模块,被集成到
Windows 和 IE 之中。到了 版时,DirectX Media 又被 DirectX SDK 抛弃(其核心
模块 DirectShow 被集成到了 DirectX 之中),其功能被随 Windows 2000 Server 推出
的 Windows Media SDK 所代替。
DirectX Transform(变换)—— 用于产生二、三维图形的各种效果,如α混色
和表面弯曲等。
DirectAnimation(动画)—— 集成多种媒体类型来创建复杂动画,并可用作
网页内容。
DirectX Video Acceleration(视频加速 DXVA)—— 利用硬件加速二维视频的
解码操作。
Direct3D Retained Mode(保留模式)—— Direct3D 的保留模式是一种高层
API,建立在 Direct3D 立即模式之上,使用起来方便容易,但是效率不高,适
合于初学者。
DirectShow(展示)—— 支持音视频流的高质量采集和重放。
编程准备
为了用 VC 编写 DirectX 应用程序,必须先下载并安装 DirectX SDK,并进行必要的头
/库文件路径和链接库的设置。
下面以 Windows XP 中文版环境下的 Visual C++ 2005 中文版和 DirectX August
2006 SDK 为例,介绍进行具体的 DirectX 编程之前,所需做的必要准备工作。
下载并安装 DirectX SDK
可以从微软公司的官方网站(
DirectX SDK,如 (448MB)。安装完成后,会在安装目录(缺省为
“C:\Program Files\Microsoft DirectX SDK ((December 2006)”)中生成 Developer Runtime、
Documentation、Include、Lib、Redist、Samples 和 Utilities 等多个子目录。安装时还会为 Windows
操作系统生成名为“Microsoft DirectX SDK ((December 2006)”的程序组,包含多个子程序组和
程序。
安装程序会将 DirectX SDK 的帮助文档,也安装到 Visual Studio 2005 的帮助系统内,位
于其“目录/DirectX SDK ((December 2006)”目录之中,包含“DirectX SDK ((December 2006)
C++”和“DirectX SDK ((December 2006) Managed”两个帮助子目录。
设置包含和库路径
在 Visual Studio 2005 中文版的 IDE 中,选“工具/选项”菜单项,打开“选项”对话框(参
见图 13-5)。
图 13-5 Visual Studio 2005 的“选项”对话框
再选该对话框左边的“项目和解决方案/VC++目录”路径,在右边的“显示以下内容的目录”
栏的下拉式列表框中,选“包含文件”项。按该栏下面的“新行”钮 ,则在其下方的显示栏
中会出现新行,再按新行右端的带省略号的浏览钮 ,在弹出的“选择目录”对话框中,选
择 DirectX SDK 的 Include 目录所在的路径“C:\Program Files\Microsoft DirectX SDK (August
2006)\Include”,然后按“打开”关闭“选择目录”对话框。结果参见图 13-6。
图 13-6 设置 DirectX 的“包含文件”的路径
可类似选择“库文件”项,添加库文件路径“C:\Program Files\Microsoft DirectX SDK (August
2006)\Lib\x86”,结果参见图 13-7。最后,按“确定”钮,关闭“选项”对话框。
图 13-7 设置 DirectX 的“库文件”的路径
注意,对 DirectX 的包含路径和库路径的设置,对所有项目都有效。
设置链接库
先创建一个 DirectX 应用程序的 MFC 单文档项目(如 DxTest),再在 VS05 的 IDE 中,
选“项目/属性”,打开“DxTest 属性页”对话框。在左上角的“配置”栏中,选中“所有配置”项;
再选左边的路径“配置属性/链接器/输入”,在右边的“附加依赖项”栏中,添加
等链接库(在多个库文件之间,用空格分隔)。具
体添加哪些 DirectX 库,与具体应用程序所使用的 DirectX 模块有关,你也可以查看 DirectX
的库目录。最后,按“应用”钮和“确定”钮,关闭属性页对话框。参见图 13-8。
图 13-8 设置 DirectX 的链接库
注意,对 DirectX 链接库的设置,只对 VC05 中当前打开的单个项目有效。
在安装了 DirectX SDK,并设置好文件路径和链接库后,就可以开始编写具体的 DirectX
应用程序了。
DirectX 的组件众多、功能强大、编程复杂,完整的介绍需要几本大部头的专著才行,
本书只准备简单介绍 DirectX 的最主要模块——Direct3D 和 DirectSound 编程的最基本内容。
Direct3D 编程简介
Direct3D(直接三维)是 DirectX 的核心模块,新版本的 Direct3D 中还包含原来
DirectDraw 模块的 2D 功能。因此,现在的 Direct3D 是 DirectX Graphics 的唯一模块,具有
DirectX(二、三维)图形的全部功能。
流水线与设备
Direct3D 用图形流水线(graphics pipeline)来处理和渲染场景(render scene),而
Direct3D 设备(device)是执行变换(transformation)和光照(lighting)等操作的渲染部件。
Direct3D 图形流水线
可通过 Direct3D 图形流水线来充分利用硬件性能,以提供高效处理和绘制 Direct3D 场
景到显示器的能力。图 13-9 为流水线(pipeline,管线/管道/流程)的组成模块框图。
图 13-9 Direct3D 图形流水线
Direct3D 设备
Direct3D 设备(device)是 Direct3D 的渲染部件,它封装和存储渲染状态。另外,它还
执行变换和光照等操作,并且光栅化图像到表面(如屏幕)。在结构上,Direct3D 设备包含
变换模块( transformation module )、光照模块
(lighting module )和光栅化器(rasterizer )。图
13-10 为 Direct3D 的设备架构(architecture)。
当前,Direct3D 有两种主要的设备类型:
HAL 设备(hal device) —— 具有硬件加速的光栅化(rasterization),可同时使用硬
件和软件的顶点处理进行着色(shading)。
参考设备(reference device)—— 由软件驱动程序提供,现在主要用于性能测试。
具有软件特性的参考设备不一定能提供与硬件等同的性能,因此应用程序必须总是查
询设备的能力以确定其支持哪些特性。
基本编程过程
Direct3D 的基本编程过程如图 13-11 所
示。下面各小节依次介绍 Direct3D 编程的基
本步骤和方法。
创建和初始化对象
为了使用 Direct3D,你必须首先创建一个
应用窗口,然后再创建和初始化 Direct3D 对象。
利用这些对象所实现的 COM 接口来操作它
们,以创建绘制场景所需的其它对象。
如果进行 MFC 编程,则可以省略创建窗
口这一步,而直接使用视图类所代表的客户
区窗口。
下面介绍的是初始化 Direct3D 的内容,
包括创建 Direct3D 对象、创建 Direct3D 设备
和设置 Direct3D 设备的渲染(render 着色)状
顶点数据
图元数据
镶嵌 顶点处理 几何处理 像素处理 像素绘制
纹理采样器
纹理表面
变换模块 光照模块 光栅化器
Direct3D 设备架构
图 13-10 Direct3D 设备架构
创建 Direct3D 对象
创建 Direct3D 设备对象
设置渲染状态
定义顶点格式
创建顶点缓冲区
锁定顶点缓冲区
解锁顶点缓冲区开始场景
结束场景
构造几何对象
设置材质和光照或纹理
设置设备的流源和顶点格式
绘制图元
展现场景
进行世界、观察和投影矩阵变换
清除对象
清除显示表面
图 13-11 Direct3D 编程的基本过程
态等。
创建 Direct3D 对象
可使用全局函数 Direct3DCreate9 来创建 Direct3D 接口对象:
IDirect3D9 * Direct3DCreate9( // 成功返回指向 IDirect3D9 接口的指针,失败返回 NULL
UINT SDKVersion // 必须为 D3D_SDK_VERSION
);
例如:IDirect3D9 * g_pD3D = Direct3DCreate9( D3D_SDK_VERSION );
创建 Direct3D 设备
可使用 IDirect3D9 接口的 CreateDevice 方法来创建设备(尾参数的返回值):
HRESULT CreateDevice( // 成功返回 D3D_OK(= 0)
UINT Adapter, // 显卡(display adapter)序号,D3DADAPTER_DEFAULT 为主显
卡
D3DDEVTYPE DeviceType, // 设备的枚举类型,一般取为 D3DDEVTYPE_HAL
HWND hFocusWindow, // 焦点窗口句柄,可取为 this->m_hWnd
DWORD BehaviorFlags, // 控制设备的创建行为之选项组合,可以采用硬件或软
件
// 的顶点处理,如 D3DCREATE_SOFTWARE_VERTEXPROCESSING
D3DPRESENT_PARAMETERS * pPresentationParameters, // 指向展现参数结构
// D3DPRESENT_PARAMETERS 的指针
IDirect3DDevice9 ** ppReturnedDeviceInterface // 代表被创建设备的
// IDirect3DDevice9 返回接口的指针地址
);
其中,展现参数结构 D3DPRESENT_PARAMETERS 的定义为:
typedef struct D3DPRESENT_PARAMETERS { // 展现参数结构
UINT BackBufferWidth, BackBufferHeight; // 后台缓冲区的宽高(以像素为单位),
// 对窗口模式可取为 0,对全屏模式则必须与当前显卡枚举模式的一致
D3DFORMAT BackBufferFormat; // 后台缓冲区的格式,可取为
// D3DFMT_UNKNOWN(= 0)
UINT BackBufferCount; // 后台缓冲区计数,可取为 0~3,取 0 时等价于 1
D3DMULTISAMPLE_TYPE MultiSampleType; // 多样本类型,一般取为
// D3DMULTISAMPLE_NONE(= 0)
DWORD MultiSampleQuality; // 多样本质量水平,一般取 0
D3DSWAPEFFECT SwapEffect; // 交换效果,可取为
// D3DSWAPEFFECT_DISCARD(= 1)、D3DSWAPEFFECT_FLIP(=
2)
// 和 D3DSWAPEFFECT_COPY(= 3)等枚举值
HWND hDeviceWindow; // 设备窗口的句柄,对窗口模式可取为 NULL(= 0)
BOOL Windowed; // 应用程序在窗口/全屏中运行时取 TRUE/FALSE
BOOL EnableAutoDepthStencil; // 允许自动深度模板,为 TRUE 则由 Direct3D
// 来管理应用程序的深度缓冲区
D3DFORMAT AutoDepthStencilFormat; // 自动深度模板格式,可取
// D3DFMT_D16、D3DFMT_D24S8 和 D3DFMT_D32 等枚举值
DWORD Flags; // 表现标志 D3DPRESENTFLAG 常数之一,可取为 0
UINT FullScreen_RefreshRateInHz; // 全屏时的屏幕刷新率,对窗口模式必须为 0
UINT PresentationInterval; // 前后台缓冲区交换的最大速率,可取为
// D3DPRESENT_INTERVAL_DEFAULT(= 0)
} D3DPRESENT_PARAMETERS;
例如:
// 设置创建 D3D 设备用的表现结构
D3DPRESENT_PARAMETERS d3dpp; // 定义表现结构变量
ZeroMemory( &d3dpp, sizeof(d3dpp) ); // 内存清零
= TRUE; // 窗口模式
= D3DSWAPEFFECT_DISCARD; // 放弃交换效果
= D3DFMT_UNKNOWN; // 未知格式
= TRUE; // 允许自动深度模板
= D3DFMT_D16; // 16 位深度缓冲
LPDIRECT3DDEVICE9 g_pd3dDevice; // 定义 D3D 设备接口的指针
// 创建 D3D 设备对象(获取 D3D 设备接口的指针)
g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
this->m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice );
设置设备的渲染状态
利用 IDirect3D9 接口的 CreateDevice 方法所返回的设备接口 IDirect3DDevice9 的指针,
可以调用该接口的设置渲染(render 着色)状态方法 SetRenderState,来初始化设备:
HRESULT SetRenderState( // 成功返回 D3D_OK(= 0)
D3DRENDERSTATETYPE State, // 设备状态
DWORD Value // 设置值
);
其中,设备状态 State,可取 D3DRENDERSTATETYPE 枚举的任意值,如 D3DRS_ZENABLE
(允许 Z 缓冲)、D3DRS_FILLMODE(填充模式)、D3DRS_SHADEMODE(阴影模式)、
D3DRS_ALPHATESTENABLE (允许α 测试)、D3DRS_CULLMODE (剔除模式)、
D3DRS_FOGENABLE ( 允 许 雾 化 ) 、 D3DRS_LIGHTING ( 光 照 ) 、
D3DRS_DITHERENABLE(允许抖动)、……。
例如:
g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ); // 关闭背面剔除
g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE ); // 允许 Z 缓冲
g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE ); // 关闭光照
创建场景
三维场景是由若干物体(几何对象)构成的,
复杂的几何对象由多个简单的图元(点、线、三
角形和多边形等)所组成,而图元(primitive)
又是顶点(vertex)的集合。
3D 图元一般是一个多面体,通常用其表面的
三角网格来表示,参见图 13-12。所以最基本的图元是三角形和顶点。
创建场景中的几何对象,等价于建立表示该几何体的顶点集合。因此,必须先定义自己
的顶点结构并创建顶点缓冲区,然后在锁定顶点缓冲区后,再通过给顶点缓冲区中的各顶点
结构赋值来构造几何对象,最后解锁顶点缓冲区。
定义顶点结构
Direct3D 使用一种 FVF(Flexible Vertex Format 可变顶点格式)结构来定义顶点,以满
足用户的不同需要。在 FVF 结构 CUSTOMVERTEX 中,可以包含描述顶点的位置、RHW
(reciprocal homogeneous w 倒数齐次 w,表示已变换顶点位置的齐次坐标 w 分量)、法向、
大小和(漫射、镜面反射)颜色等字段,具体内容由用户根据需要自己来确定,但是各字段
之间的相对顺序不能改变。
其中,顶点的位置和法向等字段,一般为三维点向量,可以用 Direct3D 的扩展三维向量
结构 D3DXVECTOR3 来表示:
typedef struct D3DXVECTOR3 { FLOAT x; FLOAT y; FLOAT z;} D3DXVECTOR3;
其中的 FLOAT 为 float 的 typedef 类型,定义在 头文件中。而颜色一般采用 DWORD
的 typedef 定义类型 D3DCOLOR。
例如:
struct CUSTOMVERTEX { // 含位置、法向量和颜色的自定义顶点结构
D3DXVECTOR3 position; // 未变换顶点的 3D 位置
D3DXVECTOR3 normal; // 顶点的表面法向量
D3DCOLOR color; // 颜色
}; 或:
struct CUSTOMVERTEX { // 含位置、颜色和纹理坐标的自定义顶点结构
D3DXVECTOR3 position; // 未变换顶点的 3D 位置
D3DCOLOR color; // 颜色
FLOAT tu, tv; // 纹理坐标
}; 或:
struct CUSTOMVERTEX { // 含位置齐次坐标和颜色的自定义顶点结构
图 13-12 3D 图元表面的三角网格
FLOAT x, y, z, rhw; // 已变换顶点的 3D 位置
DWORD color; // 颜色
};
用户在定义了自己的顶点结构类型后,还需要定义一个对应的(宏)常量,来描述该结
构的字段内容。该(宏)常量由若干表示顶点格式的标志位符号常量(参见表 13-1)的位或
组合而成。
表 13-1 顶点数据格式标志
符号常量 值 数据类型 描述
D3DFVF_XYZ 0x002 float, float, float 未转换的顶点坐标
D3DFVF_XYZRHW 0x004 float, float, float, float 已转换的顶点坐标
D3DFVF_XYZB1~5 0x006/8/a/c/
e
1- 3 个 float 含权重 i 的顶点坐标
D3DFVF_XYZW 0x4002 float, float, float, float 已转换和裁剪的顶点坐标
D3DFVF_NORMAL 0x010 float, float, float 顶点的法线
D3DFVF_PSIZE 0x020 float 点大小
D3DFVF_DIFFUSE 0x040 DWORD(ARGB) 漫射颜色
D3DFVF_SPECULAR 0x080 DWORD(ARGB) 镜面反射颜色
D3DFVF_TEX0~8 0x0~800 1- 4 个 float i 重纹理映射
例如:
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE)
或:#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
或:const DWORD FVF = (D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
创建顶点缓冲区
在定义了顶点结构后,就可以创建顶点缓冲区了,这可以由 IDirect3DDevice9 接口的
CreateVertexBuffer 方法来完成:
HRESULT CreateVertexBuffer( // 成功返回 D3D_OK(= 0)
UINT Length, // 缓冲区的字节大小,一般为 n*sizeof (CUSTOMVERTEX)
DWORD Usage, // 缓冲区用法的附加属性标志,一般为 0
DWORD FVF, // 顶点结构格式常量
D3DPOOL Pool, // 缓冲区所在的内存池类型(D3DPOOL 枚举值)
IDirect3DVertexBuffer9** ppVertexBuffer, // 返回被创建的顶点缓冲区指针
HANDLE* pSharedHandle // 保留,设为 NULL
);
其中,内存池类型 D3DPOOL 枚举的定义为:
typedef enum D3DPOOL { // 内存池类型枚举
D3DPOOL_DEFAULT = 0, // AGP 存储器或显存
D3DPOOL_MANAGED = 1, // AGP 存储器或显存,且在系统内存中还有一份拷贝
D3DPOOL_SYSTEMMEM = 2, // 系统内存
D3DPOOL_SCRATCH = 3, // 系统内存中的临时池
D3DPOOL_FORCE_DWORD = 0x7fffffff // 未使用(强制该枚举被编译成 32 位)
} D3DPOOL;
例如:g_pd3dDevice->CreateVertexBuffer( 100 * sizeof (CUSTOMVERTEX), 0,
D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL );
或: g_pd3dDevice->CreateVertexBuffer( 3 * sizeof (CUSTOMVERTEX), 0, FVF,
D3DPOOL_MANAGED, &g_pVB, NULL );
构造点集
创建三维场景中的几何对象,需要计算和构造描述该几何对象的点集。具体做法是:先
定义一个顶点指针(用于操作顶点数组);再调用 Lock 方法来获取顶点缓冲区的首地址(赋
值给顶点指针),并锁定该区域仅供当前操作使用;然后利用该顶点指针,将计算的顶点结
构值填入顶点缓冲区,从而构造出几何对象;最后调用 Unlock 方法解锁顶点缓冲区。
IDirect3DVertexBuffer9 接口的 Lock 和 Unlock 方法的定义为:
HRESULT Lock( // 成功返回 D3D_OK(= 0)
UINT OffsetToLock, // 锁定的偏移地址
UINT SizeToLock, // 锁定的字节数
VOID ** ppbData, // 返回被锁定的缓冲区头的指针
DWORD Flags // 锁定方式标志,可为 0 或下列符号常量的组合:
// D3DLOCK_DISCARD、D3DLOCK_NO_DIRTY_UPDATE、
// D3DLOCK_NOSYSLOCK、D3DLOCK_READONLY、
// D3DLOCK_NOOVERWRITE
);
HRESULT Unlock(); // 成功返回 D3D_OK(= 0)
例如:
CUSTOMVERTEX* pVertices;
g_pVB->Lock( 0, 0, (void**)&pVertices, 0 );
…… // 给顶点缓冲区中的各顶点结构赋值(构造几何对象的点集)
g_pVB->Unlock();
矩阵变换
描述单个物体(3D 几何对象)一般采用的是本地相对坐标。因此,在渲染之前,需要
将它们都转换为描述场景的统一世界空间(world space)坐标,这对应于世界矩阵变换。
为了将三维场景显示在二维屏幕上,还需要选择一个观察点(眼睛/摄像机)和观察角度
(方向/坐标)(对应于观察坐标矩阵变换),并将 3D 场景投影到一个 2D 平面(屏幕)上
(对应于投影矩阵变换),还要将超出屏幕的部分去掉(裁剪),也可以将场景投射到屏幕
的某个矩形区域中(视口缩放)。参见图 13-13。
图 13-13 Direct3D 的变换引擎
下面依次对这些变换加以介绍,这需要首先介绍 Direct3D 中的矩阵定义和变换方法。
矩阵与变换
在 Direct3D 中,定义了矩阵(matrix)结构 D3DXMATRIX :
typedef struct D3DXMATRIX { // 矩阵结构
struct { // 注意,C/C++的变量名可以下划线开头(但不能以数字开头)
FLOAT _11; FLOAT _12; FLOAT _13; FLOAT _14;
FLOAT _21; FLOAT _22; FLOAT _23; FLOAT _24;
FLOAT _31; FLOAT _32; FLOAT _33; FLOAT _34;
FLOAT _41; FLOAT _42; FLOAT _43; FLOAT _44;
};
} D3DXMATRIX;
对应于 4*4 的浮点矩阵:
在 Direct3D 中,还定义了一个对 Intel P4 CPU 运算进行了优化的 16 字节对齐的矩阵结
构(从 D3DXMATRIX 派生的 C++结构对象):
typedef struct _D3DXMATRIXA16 : public D3DXMATRIX { // 16 字节对齐矩阵结构
_D3DXMATRIXA16();
_D3DXMATRIXA16( CONST FLOAT * f);
_D3DXMATRIXA16( CONST D3DMATRIX& m);
_D3DXMATRIXA16( FLOAT _11, FLOAT _12, FLOAT _13, FLOAT _14,
FLOAT _21, FLOAT _22, FLOAT _23, FLOAT _24,
FLOAT _31, FLOAT _32, FLOAT _33, FLOAT _34,
FLOAT _41, FLOAT _42, FLOAT _43, FLOAT _44 );
void* operator new(size_t s);
void* operator new[](size_t s);
void operator delete(void* p);
void operator delete[](void* p);
struct _D3DXMATRIXA16& operator=(CONST D3DXMATRIX& rhs);
} _D3DXMATRIXA16;
typedef D3DX_ALIGN16 _D3DXMATRIXA16 D3DXMATRIXA16;
Direct3D 还提供了若干全局矩阵函数,如:
世界
变换
观察
变换
投影
变换
裁剪与
视口缩放
顶点 光栅器
变换引擎
44_43_42_41_
34_33_32_31_
24_23_22_21_
14_13_12_11_
D3DXMATRIX * D3DXMatrixIdentity( D3DXMATRIX * pOut ); // 创建单位矩阵
FLOAT D3DXMatrixDeterminant(CONST D3DXMATRIX * pM); // 返回矩阵的行列式值
D3DXMATRIX * D3DXMatrixInverse( // 计算逆矩阵
D3DXMATRIX * pOut, // 结果矩阵
FLOAT * pDeterminant, // 行列式值,可置为 NULL
CONST D3DXMATRIX * pM // 原矩阵
);
D3DXMATRIX * D3DXMatrixMultiply( // 两矩阵相乘
D3DXMATRIX * pOut, // 结果矩阵
CONST D3DXMATRIX * pM1, // 矩阵 1
CONST D3DXMATRIX * pM2 // 矩阵 2
);
矩阵变换(transform)的实施,可采用 IDirect3DDevice9 接口提供的 SetTransform 方法:
HRESULT SetTransform( // 成功返回 D3D_OK(= 0)
D3DTRANSFORMSTATETYPE State, // 变换类型状态值
CONST D3DMATRIX * pMatrix // 变换矩阵
);
其 中 State 的 取 值 可 为 D3DTRANSFORMSTATETYPE 枚 举 类 型 或
D3DTS_WORLDMATRIX 宏 的 值 。 如 : D3DTS_VIEW ( = 2 , 观 察 变 换 ) 、
D3DTS_PROJECTION(= 3,投影变换)、D3DTS_TEXTURE0~7(= 16~23,纹理变换)、
D3DTS_WORLD(= 256,世界变换)等。
例如:
D3DXMATRIXA16 matWorld; // 定义矩阵结构对象
D3DXMatrixIdentity( &matWorld ); // 变成单位矩阵
…… // 对 matWorld 进行其他矩阵变换
g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld ); // 实施变换
世界变换
一般用每个物体各自的本地坐标来描述所对应的 3D 几何对象,而用统一的世界坐标来
描述整个场景(参见图 13-14),渲染之前需要将顶点从本地坐标系转换到世界坐标系。称
这种从本地坐标到世界坐标的转换为世界变换(world transform),世界变换包含平移
(translation)、旋转(rotation)和缩放(scaling)等三种具体变换类型,它们都可用对应的
矩阵变换来表示。
下面是 Direct3D 提供的若干世界矩阵变换的全局函数:
D3DXMATRIX * D3DXMatrixTranslation( // 平移变换
D3DXMATRIX * pOut, // 原/结果矩阵
FLOAT x, // x 偏移量
FLOAT y, // y 偏移量
图 13-14 世界变换
FLOAT z // z 偏移量
);
D3DXMATRIX * D3DXMatrixRotationX [Y/Z] ( // (绕 x[y/z]轴的)旋转变换
D3DXMATRIX * pOut, // 原/结果矩阵
FLOAT Angle // 旋转角度(弧度单位)
);
D3DXMATRIX * D3DXMatrixScaling( // 缩放变换
D3DXMATRIX * pOut, // 原/结果矩阵
FLOAT sx, // x 方向的比例因子
FLOAT sy, // y 方向的比例因子
FLOAT sz // z 方向的比例因子
);
可以通过这些函数,获得所需要的各种变换矩阵。最后,还须使用 D3DTS_WORLD 状
态值,来调用 IDirect3DDevice9 接口的 SetTransform 方法,以实施最终矩阵所对应的世界变
换。例如:
D3DXMatrixTranslation(&matWorld, m_xPos, m_yPos, m_zPos); // 平移
D3DXMatrixRotationX( &matWorld, m_fPitch ); // 绕 x 轴旋转
g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld ); // 实施世界变换
观察变换
观察变换(view transform),可在世界坐标系(xw, yw, zw)中设置和改
变观察者的位置和方向,将世界空间中的顶点转换到摄影空间(camera
space)(xe, ye, ze),参见图 13-15。
在 Direct3D 中,观察矩阵由全局助手函数 D3DXMatrixLookAtLH
(Left-Handed,左手系)和 D3DXMatrixLookAtRH(Right-Handed,右
手系)来创建:
D3DXMATRIX * D3DXMatrixLookAtLH( // 创建左手系观察矩阵(右手系:LH→
RH)
D3DXMATRIX * pOut, // 结果矩阵
CONST D3DXVECTOR3 * pEye, // 眼睛位置点
CONST D3DXVECTOR3 * pAt, // 观察目标点(方向),通常取为原点 (0, 0, 0)
CONST D3DXVECTOR3 * pUp // 观察上方点,通常取为 y 轴正向 (0, 1, 0)
);
可以通过这两个函数之一(一般采用左手系的观察矩阵函数),来获得所需要的观察变
换矩阵。最后,也是需要使用 D3DTS_VIEW 状态值,来调用 IDirect3DDevice9 接口的
SetTransform 方法,以实施矩阵所对应的观察变换。例如:
D3DXMATRIX matView; // 定义观察矩阵结构
D3DXVECTOR3 eye(2, 3, 3); // 设眼睛位置为(2, 3, 3)
图 13-15 观察变换
D3DXVECTOR3 at(0, 0, 0); // 设目标点为原点
D3DXVECTOR3 up(0, 1, 0); // 设 y 轴正向为上方
D3DXMatrixLookAtLH(&matView, &eye, &at, &up); // 获取观察变换矩阵
g_pd3dDevice->SetTransform( D3DTS_VIEW, &matView ); // 实施观察变换
投影变换
投影变换(projection transform)将观察平截头体(frustum,切割物体的两个平面之间的
立体部分)转换为一个立方体形状(cuboid shape)。靠近观察点的物体,投影后会显得大一
些。参见图 13-16。
图 13-16 投影变换与观察平截头体 图 13-17 观察平截头体的确定
观察平截头体由 fov (field of view,视场/视野)角度和前后剪切面(front and back clipping
planes)的(用 z 坐标指定的)
距离来决定,参见图 13-17。
投影矩阵是典型的缩放和
透视投影(scale and perspective
projection)变换,它将观察平截
头体转换为一个立方体,其原点
也从右上角移到了中心。参见图
13-18。
Direct3D 提供了下列 6 个全局函数来创建投影矩阵:
创建左手系透视投影矩阵——D3DXMatrixPerspectiveLH;
创建右手系透视投影矩阵——D3DXMatrixPerspectiveRH;
创建基于视野的左手系透视投影矩阵——D3DXMatrixPerspectiveFovLH;
创建基于视野的右手系透视投影矩阵——D3DXMatrixPerspectiveFovRH;
创建定制的左手系透视投影矩阵——D3DXMatrixPerspectiveOffCenterLH。
创建定制的右手系透视投影矩阵——D3DXMatrixPerspectiveOffCenterRH。
其中最常用的是创建基于视野的左手系透视投影矩阵函数:
D3DXMATRIX * D3DXMatrixPerspectiveFovLH( // 返回结果矩阵
D3DXMATRIX * pOut, // 结果矩阵
FLOAT fovY, // 视角(弧度值)
FLOAT Aspect, // 水平视角与垂直视角的比例
FLOAT zn, // 近观察平面(前剪切面)的 Z 值(距离)
图 13-18 投影变换将观察平截头体转换为一个立方体
FLOAT zf // 远观察平面(后剪切面)的 Z 值(距离)
);
其返回矩阵为:
其中:yScale = tan(2 / fovY)、xScale = yScale / Aspect
与 世 界 变 换 和 观 察 变 换 一 样 , 在 获 得 了 投 影 变 换 矩 阵 后 , 也 需 要 使 用
D3DTS_PROJECTION 状态值,来调用 IDirect3DDevice9 接口的 SetTransform 方法,以实施
该矩阵所对应的投影变换。例如:
D3DXMATRIXA16 matProj; // 定义矩阵结构
// 设置基于视野的左手系透视投影矩阵(45 度视角、水平与垂直视角的比例为 1、
// Z 值从 1~100)
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, , , );
g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj ); // 实施投影变换
剪切和窗口缩放
视口(viewport)是一个(三维场景被投影到其中的)二维矩形。当视口小于 Direct3D
的显示表面(如屏幕)时,就需要进行裁剪(clipping)。参见图 13-19。
在 Direct3D 中,定义了视口结构:
typedef struct D3DVIEWPORT9 { // 视口结构
DWORD X; // 视口矩形左上角的 x 坐标
DWORD Y; // 视口矩形左上角的 y 坐标
DWORD Width; // 视口矩形的宽度
DWORD Height; // 视口矩形的高度
float MinZ; // 最小深度缓冲值
float MaxZ; // 最大深度缓冲值
} D3DVIEWPORT9;
可以用 IDirect3DDevice9 接口的 SetViewport 和 GetViewport 方法来设置和获取视口:
HRESULT SetViewport( CONST D3DVIEWPORT9 * pViewport ); // 设置视口
HRESULT GetViewport( D3DVIEWPORT9 * pViewport ); // 获取视口
例如:
D3DVIEWPORT9 vp(0, 0, 800, 600, 0, 1);
g_pd3dDevice->SetViewport(&vp);
)/(*000
1)/(00
000
000
zfznzfzn
znzfzf
yScale
xScale
图 13-19 显示表面与视口(结构)
光照、材质与纹理
几何对象只有形状和大小,而真实的物体还有材质、纹理、重量、刚度、弹性等等其他
物理特性,我们这里只讨论影响其外观的材质和纹理。
当然,任何物体,如果没有光线照射,你是什么也看不见的。所以,场景的最后观看效
果,还取决于灯光的设置。
下面简单介绍 Direct3D 中的光照安排、材质设置和纹理使用等的基本内容。
光照
光照(light 灯光)用于照亮三维场景中的物体。Direct3D 提供了两种灯光模式:环境光
和直接光。环境光(ambient light)是一种散射的光,没有固定的方向,也不会产生阴影,可
以照亮场景的任何地方。例如阴天。
直接光(direct light)则有固定的方向,会产生阴影,只能照亮一定范围内物体的朝光表
面。例如灯光、手电筒
光和阳光,它们分别对
应于直接光中的(四散)
点光(point light )、
(圆锥)聚光(spotlight)
和 ( 平 行 ) 方 向 光
(directional light)这三种
具体类型,参见图 13-20。
在 Direct3D 中定义了光结构 D3DLIGHT9 :
typedef struct D3DLIGHT9 { 光结构
D3DLIGHTTYPE Type; // 光类型
D3DCOLORVALUE Diffuse; // 光发出的漫射色
D3DCOLORVALUE Specular; // 光发出的反射色
D3DCOLORVALUE Ambient; // 光发出的环境色
D3DVECTOR Position; // 光源位置点,方向光时被忽略
D3DVECTOR Direction; // 光的方向(非 0 矢量),点光时被忽略
float Range; // 范围:光照距离的最大值,必须≤最大浮点值的平方根,方向光时被忽略
float Falloff; // 下降:聚光时的内角到外角的光线衰减值(~),一般取为
float Attenuation0; // 衰减 0:光线强度和距离关系的计算权值 0,取值 ~ ∞
float Attenuation1; // Direct3D 中光强衰减倍数 A 的计算公式(D 为到光源的距离):
float Attenuation2; //
float Theta; // θ:聚光的内角大小
float Phi; // φ:聚光的外角大小
} D3DLIGHT9;
210
1
2 nAttenuatioDnAttenuatioDnAttenuatio
A
聚光
外角φ
内角θ
光照区
方向光点光
图 13-20 直接光的种类
其中:光类型枚举 D3DLIGHTTYPE 定义为:
typedef enum D3DLIGHTTYPE { // 光类型枚举
D3DLIGHT_POINT = 1, // 点光
D3DLIGHT_SPOT = 2, // 聚光
D3DLIGHT_DIRECTIONAL = 3, // 方向光
D3DLIGHT_FORCE_DWORD = 0x7fffffff, // 强制此枚举类型的值为双字
} D3DLIGHTTYPE;
浮点颜色值结构 D3DCOLORVALUE 的定义为:(其中各字段的取值范围为 ~)
typedef struct D3DCOLORVALUE { float r; float g; float b; float a; } D3DCOLORVALUE;
其中,a 为表示透明度的α值。矢量结构 D3DVECTOR 的定义为:
typedef struct D3DVECTOR { float x, y, z; } D3DVECTOR;
在定义并初始化了光结构后,可以利用 IDirect3DDevice9 接口的 SetLight 方法,来设置
场景的各个灯光:
HRESULT SetLight( // 成功返回 D3D_OK
DWORD Index, // 灯光的索引值(下同)
CONST D3DLight9 * pLight // 灯光的结构
);
可以对一个场景设置多个灯光,它们用各自的索引值来标识。对同一索引值的再次灯光
设置,将会覆盖掉原来已有的设置。还可以利用 IDirect3DDevice9 接口的 GetLight 方法,来
保存场景的灯光设置:
HRESULT GetLight( DWORD Index, D3DLight9 * pLight );
注意,Direct3D 允许打开和关闭所设置的各个灯光,这可由 IDirect3DDevice9 接口的
LightEnable 方法来完成:
HRESULT LightEnable( // 成功返回 D3D_OK
DWORD LightIndex, // 灯光的索引值
BOOL bEnable // TRUE:开,FALSE:关
);
还可以利用 IDirect3DDevice9 接口的 GetLightEnable 方法,来获取场景中的灯光开关情况:
HRESULT GetLightEnable( DWORD Index, BOOL * pEnable /*返回值*/ );
例如:
D3DLight9 d3dLight; // 定义灯光结构变量
ZeroMemory(&D3dLight, sizeof(d3dLight)); // 初始化为 0
// 设置白色电光
= D3DLIGHT_POINT;
= ; = ; = ;
= ; = ; = ;
= ; = ; = ;
// 设置光源位置
= ; = ; = ;
= ; // 无衰减
= ; // 最大距离
if ( g_pd3dDevice->SetLight(0, &d3dLight) == D3D_OK) // 设置灯光
g_pd3dDevice->LightEnable( 0, TRUE ); // 打开灯光
材质
材质(material)是构成物体的材料和质地(如金属、木头、布料、镜子等),它决定了
物体表面队光线的反应:
如何反射光线,包括如何进行镜面反射和漫反射;
在高光部分如何反射光线;
是否自己发出光线
没有设置材质的物体,不能反射任何光线。因此,即使场景中有光源,物体仍然显示出
一片黑暗,你什么也不会看到。所以,只有给场景的物体指定了材质,才会在显示出正确的
光照效果来。
在 Direct3D 中,并没有给出一个与自然物体(如木材、钢铁、布料等)对应的材质表,
而是定义了一个可以设置物体反光特性的材质结构 D3DMATERIAL9:
typedef struct D3DMATERIAL9 { // 材质结构
D3DCOLORVALUE Diffuse; // 漫反射光线的颜色
D3DCOLORVALUE Ambient; // 环境反射光线的颜色
D3DCOLORVALUE Specular; // (高光部分)镜面反射光线的颜色
D3DCOLORVALUE Emissive; // 发射光线的颜色(光源)
float Power; // 镜面反射光线的强度
} D3DMATERIAL9;
可以利用 IDirect3DDevice9 接口的 SetMaterial 方法来为 Direct3D 设备设置材质:
HRESULT SetMaterial( CONST D3DMATERIAL9 * pMaterial ); // 成功返回 D3D_OK
该材质设置对后面所渲染的所有物体都有效。如果希望对不同的物体设置不同的材质,就必
须在渲染每个物体前,重新设置 Direct3D 设备为对应的材质。
你还可以利用 IDirect3DDevice9 接口的 GetMaterial 方法来获取 Direct3D 设备的当前材
质设置:HRESULT GetMaterial( D3DMATERIAL9 * pMaterial /*返回材质设置*/);
例如:
D3DMATERIAL9 mat; // 定义材质结构变量
// 设置漫反射颜色值 RGBA
= ; = ; = ; = ;
// 设置环境反射颜色值 RGBA
= ; = ; = ; = ;
// 设置镜面反射颜色值和强度
= ; = ; = ; = ;
= ;
// 设置发射颜色值 RGBA
= ; = ; = ; = ;
g_pd3dDev->SetMaterial(&mat); // 设置设备材质
纹理
纹理(texture)是物体的表面图形特征(花纹),它比单一的表面颜色更逼近真实。在
Direct3D 中,纹理实际上被定义为覆盖在物体表面的矩形平面图像。
为了将纹理覆盖在物体表面上,需要确定纹理在物体表面上的位置,称之为纹理坐标。
在 Direct3D 中,定义了一个统一的纹理坐标系,其纹理坐标的取值范围为规范为 ~。
纹理中单个的颜色值,称为纹理元素(texel = texture element)。参见图 13-21。
为了在物体表面上使用纹理,在定义顶点格式时,
必须考虑纹理坐标(x, y)。例如:
struct CUSTOMVERTEX { // 顶点结构
D3DXVECTOR3 position; // 位置
D3DCOLOR color; // 颜色
FLOAT tu, tv; // 纹理坐标
}
对应的宏常量定义也改为:
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1)
在 Direct3D 中,定义了多种纹理接口,其中最主要的是 IDirect3DTexture9。在 Direct3D
中,创建纹理的最简单的方法是,调用全局函数 D3DXCreateTextureFromFile:
HRESULT D3DXCreateTextureFromFile(// 成功返回 D3D_OK
LPDIRECT3DDEVICE9 pDevice, // 设备指针
LPCTSTR pSrcFile, // 图像文件路径串
LPDIRECT3DTEXTURE9 * ppTexture // 返回的纹理接口指针
);
来从一个图像文件来产生纹理,它所支持的图像文件格式有:BMP、DDS、DIB、HDR、
JPG、PFM、PNG、PPM 和 TGA,但是不支持 GIF 格式。
然后,我们可使用 IDirect3DDevice9 接口的 SetTexture 方法,来设置 Direct3D 设备的当
前纹理状态:(似材质设置,对后面渲染的物体有效)
HRESULT SetTexture(// 成功返回 D3D_OK
DWORD Sampler, // 采样器编号,用于可编程渲染的参考纹理和纹理层次编号
IDirect3DBaseTexture9 * pTexture // 纹理接口指针(NULL=清除纹理)
);
例如:
LPDIRECT3DTEXTURE9 g_pTexture = NULL;
图 13-21 纹理坐标与映射
if( D3DXCreateTextureFromFile ( g_pd3dDevice, "", &g_pTexture )
== D3D_OK) {
g_pd3dDevice->SetTexture( 0, g_pTexture );
}
注意,为了使纹理的设置生效,必须关闭光照。这可以调用 IDirect3DDevice9 接口的设
置渲染状态方法 SetRenderState 来完成。例如:
g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );
Direct3D 的纹理功能非常强大,相关的结构、函数、接口和方法有大几十个,涉及到纹
理的坐标、渲染模式、提交模式、凹凸映射、过滤、混合、透明处理、动画等等方方面面。
本书由于篇幅所限,不能作详细的介绍,有兴趣的读者可以参考有关的专著、文献和帮助文
档。
渲染和显示场景
在计算机内部,三维物体是用组成物体的图元顶点的三维坐标数据来表示的。为了将三
维坐标系中的场景展示在二维的显示器上,需要考虑物体的材质和纹理、场景的光照、观察
者的视点和视角等信息。渲染(rendering 绘制/着色)就是通过透视、投影、消隐、光照、光
栅化等计算和操作,将三维场景显示在二维屏幕上。
在 Direct3D 中,渲染场景的具体过程一般包括清除显示表面、打开渲染开关、进行 3D
物体和场景的渲染、关闭渲染开关、展现场景等步骤。下面对这些步骤,逐个加以介绍。
清除显示表面
为了显示场景,必须先清除显示渲染结果的显示表面。这可以通过调用 IDirect3DDevice9
接口的 Clear 方法来完成:
HRESULT Clear( // 成功返回 D3D_OK(= 0)
DWORD Count, // 需清除的矩形数目,若 pRects 为 NULL,则必须为 0
CONST D3DRECT * pRects, // 指向将被清除矩形描述结构的常数指针,
// 为 NULL 时,清除整个显示表面
DWORD Flags, // 指明要清除的表面类型
D3DCOLOR Color, // 背景色
float Z, // 清除的深度信息,取值范围:(最近)~ (最远)
DWORD Stencil // 新的模板值,在清除非模板表面时为 0
);
其中:
Durect3D 的矩形结构 D3DRECT 的定义为:
typedef struct D3DRECT { // 矩形结构
LONG x1, y1; // 矩形左上角的坐标
LONG x2, y2; // 矩形右下角的坐标
} D3DRECT;
要清除的表面类型 Flags 可取值为下列值组合:
D3DCLEAR_STENCIL:清除模板缓冲区
D3DCLEAR_TARGET:清除渲染目标
D3DCLEAR_ZBUFFER:清除 Z 缓冲区(深度缓冲区)
D3DCOLOR 定义为双字:typedef DWORD D3DCOLOR;
可以用宏 D3DCOLOR_ARGB 和 D3DCOLOR_XRGB 等来设置:
#define D3DCOLOR_ARGB(a,r,g,b) \
((D3DCOLOR)((((a)&0xff)<<24)|(((r)&0xff)<<16)|(((g)&0xff)<<8)|((b)&0xff)))
#define D3DCOLOR_RGBA(r,g,b,a) D3DCOLOR_ARGB(a,r,g,b)
#define D3DCOLOR_XRGB(r,g,b) D3DCOLOR_ARGB(0xff,r,g,b)
例如:(用蓝色清除渲染目标和 Z 缓冲区的整个显示表面)
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0, 0, 255), , 0 );
打开和关闭渲染开关
为了准备三维渲染、检查设备是否正常和内部数据是否有效,也为了防止在渲染过程中
其他操作影响与渲染相关的数据,必须在渲染前后通知 Durect3D 系统,即打开和关闭渲染开
关。这可以通过调用 IDirect3DDevice9 接口的开始和结束场景方法 BeginScene 和 EndScene
来完成:
HRESULT BeginScene(); // 开始场景
HRESULT EndScene(); // 结束场景
例如:
g_pd3dDevice->BeginScene(); // 开始场景
…… // 设置材质和光照、进行各种矩阵变换、绘制场景
g_pd3dDevice->EndScene(); // 结束场景
绘制场景
场景由若干物体构成,而物体又是由若干图元所构成,图元是 Direct3D 的最小绘制单位。
而场景及其图元,是用顶点缓冲区中的内容来表示的。所以,为了绘制(rendering,渲染)
场景,必须先告诉 Direct3D,要绘制的顶点缓冲区的位置以及你定制的顶点类型,然后才能
绘制图元以完成对场景的渲染。
通知顶点缓冲区的位置,可通过 IDirect3DDevice9 接口的设置流源方法 SetStreamSource
来完成:
HRESULT SetStreamSource( // 成功返回 D3D_OK
UINT StreamNumber, // 顶点流号(0 ~ 最大流数-1)
IDirect3DVertexBuffer9 * pStreamData, // 顶点缓冲区接口对象的指针
UINT OffsetInBytes, // 偏移字节数
UINT Stride // 步长(每个顶点的字节数)
);
IDirect3DDevice9 接口的 SetFVF 方法,可以用来设置当前顶点流中的定制的顶点类型:
HRESULT SetFVF( DWORD FVF /*可变顶点格式*/ ); // 成功返回 D3D_OK
而对图元的绘制(即场景的渲染),则可由 IDirect3DDevice9 接口的绘制图元方法
DrawPrimitive 来执行:
HRESULT DrawPrimitive( // 成功返回 D3D_OK
D3DPRIMITIVETYPE PrimitiveType, // 图元类型
UINT StartVertex, // 起始顶点
UINT PrimitiveCount // 图元数目
);
其中,PrimitiveType 可取 D3DPRIMITIVETYPE 枚举值:
typedef enum D3DPRIMITIVETYPE { // 图元类型枚举
D3DPT_POINTLIST = 1, // 点列(孤立点之集)
D3DPT_LINELIST = 2, // 线表(孤立直线段序列)
D3DPT_LINESTRIP = 3, // 线带(单一折线)
D3DPT_TRIANGLELIST = 4, // (孤立)三角形序列
D3DPT_TRIANGLESTRIP = 5, // 三角形条带,背面剔除标志自动在偶数三角形翻转
D3DPT_TRIANGLEFAN = 6, // 三角形扇
D3DPT_FORCE_DWORD = 0x7fffffff, // 强制本枚举的值类型为双字
} D3DPRIMITIVETYPE;
其中,三角形条带(triangle strip)和三角形扇(triangle fan)的含义,参见图 13-22。
例如:
g_pd3dDevice->SetStreamSource( 0, g_pVB, 0, sizeof(CUSTOMVERTEX) );
g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2*50-2 );
展现场景
渲染好的场景,被 Direct3D 绘
制到了设备的显示表面上。但是这
个表面并不是屏幕,而是位于内存
的显示缓冲区中的后台表面。为了
将渲染后的图像显示在前台表面
(屏幕)上,还需要经过展现场景
(present scene)这一步。这可以通
过调用 IDirect3DDevice9 接口的展
现方法 Present 来完成:
HRESULT Present( // 成功返回 D3D_OK(= 0)
CONST RECT * pSourceRect, // 指向源矩形表面的指针,一般取为 NULL
CONST RECT * pDestRect, // 指向目标矩形表面的指针,一般也取为 NULL
图 13-22 三角形条带(左)与三角形扇(右)
HWND hDestWindowOverride, // 目标窗口的指针,其客户区用作展现的前台表面
// 若取为 NULL,则使用表现参数结构 D3DPRESENT_PARAMETERS
// 的 hDeviceWindow 字段的值
CONST RGNDATA * pDirtyRegion // 脏区域的指针,一般为 NULL
);
例如:g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
清除对象
在结束程序前,需要释放各种对象占用的内存。这可以通过调用各个接口对象(从父接
口 IUnknown 继承下来)的 Release 方法来完成:
ULONG Release(); // 返回新的引用计数
在使用完一个接口对象后,必须释放它所占用的硬件和其它资源。调用 Release 方法,
会导致对应的接口对象引用计数减 1,当引用计数为 0 后,该对象就会被自动释放。
例如,可以在视图类的析构函数中,清除对象以释放内存:
CDxTestView::~CDxTestView() {
if ( g_pTexture != NULL ) g_pTexture->Release();
if ( g_pVB != NULL ) g_pVB->Release();
if ( g_pd3dDevice != NULL ) g_pd3dDevice->Release();
if ( g_pD3D != NULL ) g_pD3D->Release();
}
编程步骤及方法
现在将上面所讲的各个编程步骤及所使用的具体方法加以总结,汇集到图 13-23 之中。
旋转圆筒例
下面给出 Direct3D 的两个具体编程例子,都显示一个旋转圆筒的场景。只是,第一个例
子是设置材质和光照,第 2 个例子则是使用纹理。它们都是在 DirectX SDK 样例的基础上改
造而成的。
例 1(材质与光照)
创建 MFC 单文档项目 DxTest,设置 DirectX 路径和链接库。
在视图类头文件 中,包含多媒体和 Direct3D 头文件:
#include <>
#include <>
在视图类 CDxTestView 中定义类指针变量:
LPDIRECT3D9 g_pD3D; // 指向 IDirect3D9 接口结构的指针,用于创建 D3D 设备
LPDIRECT3DDEVICE9 g_pd3dDevice; // 返回的绘图设备指针
LPDIRECT3DVERTEXBUFFER9 g_pVB; // 存放顶点的缓冲区的指针
并定义类顶点结构:
struct CUSTOMVERTEX { // 自定义顶点结构
D3DXVECTOR3 position; // 顶点的未转换位置
D3DXVECTOR3 normal; // 顶点的表面法线
};
和 FVF 符号常量:
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL)
图 13-23 Direct3D 的编程步骤和方法
调用全局函数 Direct3DCreate9
来创建 Direct3D 接口对象
调用 Direct3D 接口的 CreateDevice
方法来创建 Direct3D 设备接口对象
调用 Direct3D 设备接口的
SetRenderState 方法来设置渲染状态
定义可变顶点格式 FVF 结构
调用 Direct3D 设备接口的
CreateVertexBuffer 方法
来创建顶点缓冲区
调用 Direct3D 设备接口的
Lock 方法来锁定顶点缓冲区
调用 Direct3D 设备接口的
Unlock 方法来解锁顶点缓冲区
调用 Direct3D 设备接口的
BeginScene 方法来开始场景
调用 Direct3D 设备接口的
EndScene 方法来结束场景
通过给顶点缓冲区中的顶点
结构集赋值来构造几何对象
调用 Direct3D 设备接口的
SetLight 方法来设置灯光
调用 Direct3D 设备接口的
SetTransform 方法
来进行世界、观察和投影矩阵变换
调用父接口的 Release 方法
来释放各接口对象所占用的内存
调用 Direct3D 设备接口的
Clear 方法来清除显示表面
调用 Direct3D 设备接口的
Present 方法来展现场景
调用 Direct3D 设备接口的
SetStreamSource 方法来设置流源
调用 Direct3D 设备接口的
SetFVF 方法来设置顶点格式
调用 Direct3D 设备接口的
DrawPrimitive 方法来绘制图元
调用 Direct3D 设备接口的
LightEnable 方法来开关灯光
调用 Direct3D 设备接口的
SetMaterial 方法来设置材质
调用 Direct3D 设备接口的
SetRenderState 方法来关闭光照
调用 D3DXCreateTextureFromFile
全局函数来生成纹理
调用 Direct3D 设备接口的
SetTexture 方法来设置纹理
调用全局函数
D3DXMatrixIdentity、
D3DXMatrixTranslation、
D3DXMatrixRotation*
和 D3DXMatrixScaling 等
来设置世界矩阵
调用全局函数
D3DXMatrixLookAtLH 等
来生成观察矩阵
调用全局函数
D3DXMatrixPerspectiveFovLH
等来生成透视投影矩阵
在视图类的构造函数中初始化类的指针变量:
CDxTestView::CDxTestView() {
g_pD3D = NULL;
g_pd3dDevice = NULL;
g_pVB = NULL;
}
在视图类的析构函数中,释放接口对象:
CDxTestView::~CDxTestView() {
if ( g_pVB != NULL ) g_pVB->Release();
if ( g_pd3dDevice != NULL ) g_pd3dDevice->Release();
if ( g_pD3D != NULL ) g_pD3D->Release();
}
为视图类添加初始化 Direct3D 的成员函数 InitD3D:
bool CDxTestView::InitD3D() { // 成功返回 true
// 创建 D3D 对象
if( (g_pD3D = Direct3DCreate9( D3D_SDK_VERSION )) == NULL ) return false;
// 设置创建 D3D 设备用的表现结构
D3DPRESENT_PARAMETERS d3dpp; // 定义表现结构变量
ZeroMemory( &d3dpp, sizeof(d3dpp) ); // 内存清零
= TRUE; // 窗口模式
= D3DSWAPEFFECT_DISCARD; // 放弃交换效果
= D3DFMT_UNKNOWN; // 未知格式
= TRUE; // 允许自动深度模板
= D3DFMT_D16; // 16 位深度缓冲
// 创建 D3D 设备
if( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
this->m_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice ) != D3D_OK ) return false;
// 设置设备状态
// 关闭背面剔除
g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
// 打开 Z 缓冲区
g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE );
return true;
}
为视图类添加构造圆柱面几何对象的成员函数 InitGeometry:
bool CDxTestView::InitGeometry() {
// 创建顶点缓冲区
if ( g_pd3dDevice->CreateVertexBuffer( 50*2*sizeof(CUSTOMVERTEX),
0, D3DFVF_CUSTOMVERTEX,D3DPOOL_DEFAULT, &g_pVB,
NULL ) != D3D_OK ) return false;
// 填充顶点缓冲区(构造圆柱面对象)
CUSTOMVERTEX* pVertices; // 定义顶点结构指针
if ( g_pVB->Lock( 0, 0, (void**)&pVertices, 0 ) != D3D_OK ) return false; // 上锁
for ( DWORD i = 0; i < 50; i++ ) { // 构造圆柱面对象
FLOAT theta = (2 * D3DX_PI * i) / (50 - 1);
pVertices[2 * i + 0].position = D3DXVECTOR3( sinf(theta),, cosf(theta) );
pVertices[2 * i + 0].normal = D3DXVECTOR3( sinf(theta), , cosf(theta) );
pVertices[2 * i + 1].position = D3DXVECTOR3( sinf(theta), , cosf(theta) );
pVertices[2 * i + 1].normal = D3DXVECTOR3( sinf(theta), , cosf(theta) );
}
g_pVB->Unlock(); // 解锁
return true;
}
为视图类添加初始化重写成员函数 OnInitialUpdate,在该函数中调用初始化 Direct3D 的
成员函数 InitD3D 和构造场景中圆筒几何对象的成员函数 InitGeometry,同时启动定时器以绘
制动画场景:
void CDxTestView::OnInitialUpdate() {
CView::OnInitialUpdate();
if ( !InitD3D() ) { // 初始化 Direct3D
MessageBox(L"Initialize Direct3D failed!", L"Error");
return;
}
if ( !InitGeometry() ) { // 构造圆柱面几何对象
MessageBox(L"Initialize Geometry failed!", L"Error");
return;
}
else SetTimer(1, 20, NULL); // 启动定时器(用于绘制动画场景)
}
为视图类添加设置矩阵变换的成员函数 SetupMatrices:
void CDxTestView::SetupMatrices() {
// 设置世界矩阵
D3DXMATRIXA16 matWorld; // 定义世界矩阵结构
D3DXMatrixIdentity( &matWorld ); // 获得单位矩阵
D3DXMatrixRotationX( &matWorld, timeGetTime()/ ); // 动态旋转变换
// 进行世界变换
g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );
// 设置观察矩阵
D3DXVECTOR3 vEyePt( , , ); // 眼睛位于点(0, 3, -5)
D3DXVECTOR3 vLookatPt( , , ); // 目标点为原点
D3DXVECTOR3 vUpVec( , , ); // 上方为 y 轴正向
D3DXMATRIXA16 matView; // 定义观察矩阵结构
// 获取观察矩阵
D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );
// 进行观察变换
g_pd3dDevice->SetTransform( D3DTS_VIEW, &matView );
// 设置投影矩阵
D3DXMATRIXA16 matProj; // 定义投影矩阵结构
// 获取投影矩阵(45 度视角、水平与垂直视角的比例为 1、Z 值从 1~100)
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, , , );
// 进行投影变换
g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj );
}
其中,timeGetTime 为 Windows 的 SDK 函数,返回系统时间(从系统启动到当前时刻的毫
秒数),其函数原型为:DWORD timeGetTime(void);
为视图类添加设置材质和光照的成员函数 SetupMaterialLight:
void CDxTestView::SetupMaterialLight() {
// 设置材质(漫射和环境光都为黄色)
D3DMATERIAL9 mtrl;
ZeroMemory( &mtrl, sizeof(D3DMATERIAL9) );
= = ;
= = ;
= = ;
= = ;
g_pd3dDevice->SetMaterial( &mtrl );
// 设置白色有向光,具有震荡方向
D3DXVECTOR3 vecDir;
D3DLIGHT9 light;
ZeroMemory( &light, sizeof(D3DLIGHT9) );
= D3DLIGHT_DIRECTIONAL;
= ; = ; = ;
vecDir = D3DXVECTOR3(cosf(timeGetTime()/), ,
sinf(timeGetTime()/) );
D3DXVec3Normalize( (D3DXVECTOR3*)&, &vecDir );
= ;
g_pd3dDevice->SetLight( 0, &light );
g_pd3dDevice->LightEnable( 0, TRUE );
g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );
// 打开某种环境光
g_pd3dDevice->SetRenderState( D3DRS_AMBIENT, 0x00202020 );
}
为视图类添加渲染函数 Render:
void CDxTestView::Render() {
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0,0,255), , 0 ); // 清除后台和 Z 缓冲区
if ( SUCCEEDED( g_pd3dDevice->BeginScene() ) ) { // 开始场景
SetupMaterialLight(); // 设置材质和光照
SetupMatrices(); // 进行世界、观察和投影矩阵变换
// 渲染顶点缓冲区内容
g_pd3dDevice->SetStreamSource( 0, g_pVB, 0, sizeof(CUSTOMVERTEX) );
g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2*50-2 );
g_pd3dDevice->EndScene(); // 结束场景
}
g_pd3dDevice->Present( NULL, NULL, NULL, NULL ); // 展现场景
}
为视图类添加计时器的消息响应函数,在其中调用(动态)场景渲染成员函数 Render:
void CDxTestView::OnTimer(UINT_PTR nIDEvent) {
Render();
CView::OnTimer(nIDEvent);
}
运行结果如图 13-24 所示。
例 2(纹理)
项目与程序结构同
例 1,不同之处如下:
在视图类中补充定义
纹理接口对象的指针
图 13-24 材质与光照例
类变量:
LPDIRECT3DTEXTURE9 g_pTexture;
在视图类的构造函数中初始化该变量:g_pTexture = NULL;
在视图类的析构函数的开始处,添加释放该对象所占用的内存的语句:
if( g_pTexture != NULL ) g_pTexture->Release();
在视图类定义中,修改可变顶点格式 FVF 的定制顶点结构为:
struct CUSTOMVERTEX {
D3DXVECTOR3 position; // 位置
D3DCOLOR color; // 颜色
FLOAT tu, tv; // 纹理坐标
}
同时修改对应的宏常量定义为:
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1)
在 InitD3D 函数中添加关闭光照的语句:
g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );
在 InitGeometry 函数的开始处,添加从图像文件创建纹理的语句:
if ( D3DXCreateTextureFromFile( g_pd3dDevice, L"",
&g_pTexture ) != D3D_OK ) {
MessageBox(L"Could not find ", L"Error");
return false;
}
并将 InitGeometry 函数中的,构造圆筒对象的循环改为:
for ( DWORD i=0; i<50; i++ ) {
FLOAT theta = (2*D3DX_PI*i)/(50-1);
pVertices[2*i+0].position = D3DXVECTOR3( sinf(theta),, cosf(theta) );
pVertices[2*i+0].color = 0xffffffff;
pVertices[2*i+0].tu = ((FLOAT)i)/(50-1);
pVertices[2*i+0].tv = ;
pVertices[2*i+1].position = D3DXVECTOR3( sinf(theta), , cosf(theta) );
pVertices[2*i+1].color = 0xff808080;
pVertices[2*i+1].tu = ((FLOAT)i)/(50-1);
pVertices[2*i+1].tv = ;
}
在 Render 函数中,将清除表面方法 Clear 的第 4 个调用参数(背景色),由蓝色改为青色:
D3DCOLOR_XRGB(0, 0, 255) → D3DCOLOR_XRGB(0,128,128)
并去掉对设置材质和光照函数 SetupMaterialLight 的调用,还要在调用矩阵变换函数
SetupMatrices 的 语 句 之 后 , 添 加 纹 理 设 置 语 句 : g_pd3dDevice->SetTexture( 0,
g_pTexture );
则所用的纹理图片和运行结果,如图 13-25 所示。
图 13-25 纹理例所用的纹理图片 (左)和运行结果(中、右)
DirectSound 编程简介
DirectX 的音频部分变动很大,从最初的 DirectSound()及 DirectSound 3D(),
到 后 来 的 DirectMusic ( ) , 再 到 DirectAudio ( ) , 最 后 到 现 在 的 XACT
()。这些模块之间的关系复杂,各模块的功能重叠,而且现在仍然处于不断地
发展和变化之中。
一般来说,DirectSound 是一种主要针对波形音频的高效底层接口,包含三维音频部分
(DirectSound 3D),它功能强大,是微软公司推崇的主要音频接口,也是游戏开发者的首选;
而 DirectMusic 则是一种主要针对 MIDI 音乐的高层接口,但它也可以用来创建动态波形声音,
其功能也很强大,但是效率不是太高,很少用于游戏开发,微软公司现在也不太支持和看好
它;XACT(Microsoft Cross-Platform Audio Creation Tool 微软跨平台音频生成工具)是微软
公司最新推出的游戏音频接口,有替代 DirectSound 的趋势;DirectX Audio 只是这些音频接
口的总和与统称,包括 DirectSound、DirectMusic 和 XACT 这三个组成部分。
由于篇幅所限,本节只介绍 DirectSound 的部分基本内容。
功能与模块
DirectSound(直接声音)能够非常高效地播放声音,并可让应用程序对硬件资源进行高
级别的控制。DirectSound 接口的功能有:
从 WAV 格式的文件和资源来播放声音;
同时播放多个声音;
将高优先级的声音赋值到硬件控制的缓冲区;
在可定制的 3D 环境中定位声音;
添加诸如回声和合唱等音效,并能动态改变音效参数;
从话筒或其它输入捕获(采集/录制)WAV 声音。
DirectSound 是作为一组 COM 对象实现的,既代表声音物理设备也表示声音数据缓冲区。
下面是 DirectSound 接口模块中一些涉及播放音频的主要对象(接口):
DirectSound(IDirectSound8):对象表示音频设备硬件,接口则用于创建缓冲区对象
和设置环境;
DirectSoundBuffer(IDirectSoundBuffer8):对象表示音频数据流,接口则用于管理
声音缓冲区;
DirectSound3DBuffer(IDirectSound3DBuffer8):对象表示三维音频数据流,接口则
用于获取和设置声音缓冲区的三维空间位置、方向和环境参数;
DirectSound3DDlistner(IDirectSound3DListener8):对象表示三维音频接收者(听
众),接口则用于获取和设置接收者的三维空间位置、方向和环境参数;
DirectSoundNotify(IDirectSoundNotify8):对象提供通知应用程序有关 DirectSound
事件的机制,接口则用于为播放和录制缓冲区设置通知事件。
在 DirectX 的 DirectSound 接口模块中,除了上面列出的 5 个接口外,还有其它用于捕获、
各种音效和全双工的 15 个接口。由于篇幅所限,本书仅讨论二维音频的播放,所以只需用
到上面已经列出的 5 个对象中的前两个对象——DirectSound 和 DirectSoundBuffer。
DirectSound 对象
DirectSound(直接声音)对象是物理音频设备(声卡)的软件表示,因为一个计算机一
般只有一个声卡,所以多个使用 DirectSound 的应用程序必须共享一个 DirectSound(设备)
对象,即使用同一个声卡。因为 DirectSound 会自动跟踪每个应用程序的输入焦点,使生成
的声音与有输入焦点的程序一致,所以共享 DirectSound 对象毫无问题。
创建 DirectSound 对象
应用程序可以通过调用全局函数 DirectSoundCreate8 来生成一个 DirectSound 设备对象:
HRESULT DirectSoundCreate8( // 成功返回 DS_OK(= 0)
LPCGUID lpcGuidDevice, // NULL 表示缺省声卡设备
LPDIRECTSOUND8 * ppDS8, // 返回指向 IDirectSound8 接口的指针
LPUNKNOWN pUnkOuter // 必须为 NULL
);
其中的参数 ppDS8 所返回的,正是我们操作 DirectSound 设备对象所要用的 COM 接口
IDirectSound8。我们可以调用该接口的各种方法,来获取和设置设备对象的属性、生成缓冲
区对象等等。例如:
LPDIRECTSOUND8 g_pDS = NULL; // 定义 IDirectSound8 接口指针
DirectSoundCreate8(NULL, &g_pDS, NULL); // 创建 DirectSound 对象
IDirectSound8 接口常用方法
下面是 IDirectSound8 接口的若干常用方法:
设置合作级别
// 设置应用程序对声音设备的合作级别
HRESULT SetCooperativeLevel( // 成功返回 DS_OK
HWND hwnd, // 应用程序窗口的句柄
DWORD dwLevel // 所需要的级别,一般取为 DSSCL_PRIORITY
);
注意,在播放缓冲区前必须调用该方法,通常是在获得 DirectSound 对象后立即调用。例如:
LPDIRECTSOUNDBUFFER g_pSB = NULL; // 定义 IDirectSoundBuffer8 接口指针
g_pDS->SetCooperativeLevel(this->m_hWnd, DSSCL_PRIORITY); // 设置合作水平
获取设备性能
还可以通过 IDirectSound8 接口的 GetCaps 方法来获取硬件设备(声卡)的性能:
HRESULT GetCaps( // 成功返回 DS_OK
LPDSCAPS pDSCaps // 返回的 DSCAPS 结构地址
); // 有关 DSCAPS 结构的具体内容,可参见 SDK 的帮助文档
例如:DSCAPS caps; g_pDS->GetCaps(&caps);
创建声音缓冲区
CreateSoundBuffer(创建声音缓冲区)是 DirectSound 接口的最重要方法,用来获取主声
音缓冲区和创建辅声音缓冲区。一般不需要自己创建主缓冲区,除非需为设置自己特殊的波
形音频格式而获取主缓冲区接口对象。对辅声音缓冲区,则需要提供复杂的波形格式参数。
HRESULT CreateSoundBuffer( // 成功返回 DS_OK
LPCDSBUFFERDESC pcDSBufferDesc, // 描述声音缓冲区的 DSBUFFERDESC
// 结构的指针
LPDIRECTSOUNDBUFFER * ppDSBuffer, // 返回指向声音缓冲区对象的指针
LPUNKNOWN pUnkOuter // 必须为 NULL
);
其中 DSBUFFERDESC 结构的定义为:
typedef struct DSBUFFERDESC { // 缓冲区描述结构
DWORD dwSize; // 本结构的字节大小,必须在使用前初始化
DWORD dwFlags; // 指定缓冲区性能的标志
DWORD dwBufferBytes; // 新缓冲区的字节大小,对主缓冲区必须为 0
DWORD dwReserved; // 保留,必须为 0
LPWAVEFORMATEX lpwfxFormat; // 指向指定缓冲区的波形格式的
// WAVEFORMATEX 或 WAVEFORMATEXTENSIBLE 结构的指针
// 对主缓冲区必须为 NULL
GUID guid3DAlgorithm; // DirectSound3D 硬件模拟使用的双扬声器虚拟化算法
// 的 GUID(Global Unique IDentifier 全局唯一标识符),如果没有设置
// DSBCAPS_CTRL3D 标志,则该字段必须取值 GUID_NULL
} DSBUFFERDESC;
typedef const DSBUFFERDESC *LPCDSBUFFERDESC;
其中,dwFlags 可取值表 13-2 中符号常量的位组合。
表 13-2 缓冲区性能的标志常量
符号常量 值 含义
DSBCAPS_PRIMARYBUFFER 0x00000001 主缓冲区
DSBCAPS_STATIC 0x00000002 静态缓冲区
DSBCAPS_LOCHARDWARE 0x00000004 硬件混音缓冲区
DSBCAPS_LOCSOFTWARE 0x00000008 软件混音缓冲区
DSBCAPS_CTRL3D 0x00000010 有 3D 控制能力的缓冲区
DSBCAPS_CTRLFREQUENCY 0x00000020 有频率控制能力的缓冲区
DSBCAPS_CTRLPAN 0x00000040 有面板控制能力的缓冲区
DSBCAPS_CTRLVOLUME 0x00000080 有音量控制能力的缓冲区
DSBCAPS_CTRLPOSITIONNOTIFY 0x00000100 有位置通知能力的缓冲区
DSBCAPS_CTRLFX 0x00000200 支持音效处理的缓冲区
DSBCAPS_STICKYFOCUS 0x00004000 可粘住焦点的缓冲区
DSBCAPS_GLOBALFOCUS 0x00008000 有全局焦点的缓冲区
DSBCAPS_GETCURRENTPOSITION2 0x00010000 获取当前位置 2
DSBCAPS_MUTE3DATMAXDISTANCE 0x00020000 在最大距离处声音降为无声
DSBCAPS_LOCDEFER 0x00040000 播放时可将缓冲区指派给软硬件资源
释放 DirectSound 对象
ULONG Release(); // 返回新的引用计数
它是从父接口 IUnknown 继承下来的基类成员方法。在使用完一个 DirectSound 对象后,
必须释放它所占用的声音硬件和其它资源。调用 Release 方法,会导致 DirectSound 对象引用
计数减 1,当引用计数为 0 后,DirectSound 对象就会被自动释放,它所创建的所有
DirectSoundBuffer 对象也会被自动释放。
例如:下面的代码片段,先创建 DirectSound 设备对象,再设置合作水平,最后创建(获
取)主缓冲区:
LPDIRECTSOUND8 g_pDS = NULL; // 定义 IDirectSound8 接口指针
LPDIRECTSOUNDBUFFER g_pSB = NULL; // 定义 IDirectSoundBuffer 接口指针
// 创建 DirectSound 对象
if ( DirectSoundCreate8(NULL, &g_pDS, NULL) == DS_OK ) {
// 设置合作水平
if ( g_pDS->SetCooperativeLevel(this->m_hWnd, DSSCL_PRIORITY)
== DS_OK ) {
DSBUFFERDESC dsbd; // 定义缓冲区描述结构变量
ZeroMemory( &dsbd, sizeof(DSBUFFERDESC) ); // 清零
= sizeof(DSBUFFERDESC); // 结构大小
= DSBCAPS_PRIMARYBUFFER; // 主缓冲区
= 0; // 对主缓冲区必须为 0
= NULL; // 必须为 NULL
// 创建(获取)主缓冲区
if( g_pDS->CreateSoundBuffer( &dsbd, &g_pSB, NULL ) == DS_OK ) {
…… // 设置主缓冲区的波形音频格式
…… // 创建辅缓冲区、写入波形数据、播放缓冲区
…… // 停止播放缓冲区、释放辅缓冲区
g_pSB->Release(); // 释放主缓冲区
}
}
g_pDS->Release(); // 释放 DirectSound 对象
}
IDirectSound8 接口的其它方法
IDirectSound8 接口的其他方法有:
Initialize——初始化使用 CoCreateInstance 全局函数创建的设备对象
VerifyCertification——确定设备驱动程序是否被 DirectX 认证
DuplicateSoundBuffer——创建与原缓冲区共享缓冲区存储区的新辅缓冲区
GetSpeakerConfig——获取设备的扬声器配置
SetSpeakerConfig——设置设备的扬声器配置
Compact——压缩,对本接口无效
从父接口 IUnknown 继承下来的基类方法有:
AddRef——添加引用
QueryInterface——查询接口
DirectSoundBuffer 对象
DirectSoundBuffer(直接声音缓冲区)对象表示一个波形音频数据流,它有两种形式:
主声音缓冲区(Primary Sound Buffer)——表示在物理音频设备上播放的音频缓冲;
辅声音缓冲区(Secondary Sound Buffer)——表示单个音频数据流、准备合成进主缓冲
区供输出。
一个应用程序的主缓冲
区有且只有一个,一般不需要
直接操作它;辅缓冲区必须至少
有一个,也可以有任意多个,
可以对辅缓冲区进行各种操作。
主缓冲区和辅缓冲区的关系见图 13-26。
可见主声音缓冲区相当于混音器,辅声音缓冲区相当于混音器的输入,每个辅缓冲器对
应于应用程序的单个分离的声音。
另外,DirectSoundBuffer 对象还支持如下两种类型的缓冲区:
静态(static)缓冲区——包含一个完整的声音,有效、快、常用;
流式(streaming)缓冲区——只包含声音中的一部分,要求边播放边写入新数据,一
…
辅缓冲区
主缓冲区 音频硬件 扬声器辅缓冲区
辅缓冲区
图 13-26 主辅声音缓冲区的关系
般只用于大型波形音频文件和因特网上流媒体的在线播放。
DirectSoundBuffer 对 象 是 通 过 名 为 IDirectSoundBuffer8 的 COM 接 口 来 操 纵 的 ,
DirectSoundBuffer 对象可由 DirectSound 对象调用 CreateSoundBuffer 成员方法生成,并返回
一个指向 IDirectSoundBuffer8 接口的指针。
IDirectSoundBuffer8 接口常用方法
下面是 IDirectSoundBuffer8 接口的若干常用方法:
设置主缓冲区的波形音频格式
在获取主声音缓冲区接口对象后,可以调用缓冲区接口的 SetFormat 方法,来设置主声
音缓冲区的波形音频格式:
HRESULT SetFormat( // 成功返回 DS_OK
LPCWAVEFORMATEX pcfxFormat // 指向描述主声音缓冲区格式的
// WAVEFORMATEX 结构的指针
);
其中 WAVEFORMATEX 结构的定义为:
typedef struct WAVEFORMATEX { // 波形格式结构
WORD wFormatTag; // 波形音频格式类型,一般为 WAVE_FORMAT_PCM
WORD nChannels; // 波形音频数据的声道数
DWORD nSamplesPerSec; // 采样频率(Hz)
DWORD nAvgBytesPerSec; // 所需的平均数据传输率(字节/秒)
// 对 WAVE_FORMAT_PCM 格式,应该 = nSamplesPerSec * nBlockAlign
WORD nBlockAlign; // 块对齐(字节数),对 PCM 格式应该 = (nChannels
// * wBitsPerSample) / 8;软件处理和设备读写数据,都必须以块为单位
WORD wBitsPerSample; // 样本位数,对 PCM 格式必须为 8 或 16
WORD cbSize; // 扩展结构的附加部分的大小(字节数)
} WAVEFORMATEX;
对辅缓冲区,在创建时已经设置了波形格式,所以不需要单独调用该方法。你也可以调
用该方法来重新设置主缓冲区的波形格式,但是必须重新创建辅缓冲区对象。
锁定与解锁
声音缓冲区实际上是音频数据的存储区,在播放(声音缓冲区)之前,必须先写入波形
音频数据。为此,需要获取该音频数据的存储区的地址(指针),并在写入操作时锁定该存
储区,防止其它操作使用该存储区。这些都可以通过调用 IDirectSoundBuffer8 接口的 Lock
方法来完成:
HRESULT Lock( // 成功返回 DS_OK
DWORD dwOffset, // 从缓冲区头算起的偏移量(字节数),锁定的开始处
DWORD dwBytes, // 要锁定部分的大小(字节数)
LPVOID * ppvAudioPtr1, // 返回的首个已锁定块的变量地址
LPDWORD pdwAudioBytes1, // 返回的首个已锁定块的字节大小
LPVOID * ppvAudioPtr2,// 返回的第 2 个已锁定块的变量地址
LPDWORD pdwAudioBytes2, // 返回的第 2 个已锁定块的字节大小
DWORD dwFlags // 修改锁定事件的标志,可取值为:
// DSBLOCK_FROMWRITECURSOR:从写入点开始锁定,dwOffset 参数被忽略
// DSBLOCK_ENTIREBUFFER:锁定整个缓冲区,dwBytes 参数被忽略
);
Lock 方法接受一个偏移量和一个计数,返回两个写指针及相关的锁定块大小。因为缓冲
区的大小是有限的,当数据被连续写入时,
会超过缓冲区的结尾。这时 DirectSound 并
不扩展缓冲区,而是绕回到缓冲区的头部,
循环使用缓冲区。在锁定区还没有超过缓
冲区结尾时,只需要一个写指针就够了,所
以这时第 2 个锁定区的指针为空,即
ppvAudioPtr2 = NULL(参见图 13-26 中
的上图);但是如果锁定区已经超过了缓
冲区结尾,则需要两个指针(参见图 13-26
中的下图)。如果锁定的是整个缓冲区,
则始终只需要第一个写指针即可。
在完成写入操作后,必须调用对应的 Unlock 方法来解锁声音缓冲区:
HRESULT Unlock( // 成功返回 DS_OK
LPVOID pvAudioPtr1, // 由 Lock 方法获得的 ppvAudioPtr1 变量的地址
DWORD dwAudioBytes1, // 写入位置 pvAudioPtr1 的字节数
LPVOID pvAudioPtr2, // 由 Lock 方法获得的 ppvAudioPtr2 变量的地址
DWORD dwAudioBytes2 // 写入位置 pvAudioPtr2 的字节数
);
播放与停止
// 播放辅缓冲区中的声音,从播放点(cursor 指针/指示器/游标)开始播
HRESULT Play( // 成功返回 DS_OK
DWORD dwReserved1, // 保留,必须为 0
DWORD dwPriority, // 优先级(0~0xFFFFFFFF),除采用 DSBCAPS_LOCDEFER
// 标志创建的缓冲区外,必须为 0
DWORD dwFlags // 指定任何播放缓冲区的标志,一般为 0 或 DSBPLAY_LOOPING
);
// 停止缓冲区的播放
HRESULT Stop(); // 成功返回 DS_OK
设置音量
// 设置主音量的声音衰减量
0KB 1KB 2KB 3KB 4KB
未锁定区 ↑ 锁定区 未锁定区
指针 1 (指针 2 = NULL)
0KB 1KB 2KB 3KB 4KB
↑ 锁定区 未锁定区 ↑ 锁定区
指针 2 指针 1
图 13-26 锁定与写指针构
HRESULT SetVolume( // 成功返回 DS_OK
LONG lVolume // 声音的衰减量(0~10000),单位为百分之一分贝(dB%)
);
// 设置左右声道的相对音量
HRESULT SetPan( // 成功返回 DS_OK
LONG lPan // 左右声道之间的相对音量(dB%),取值范围为(0 为均衡):
// DSBPAN_LEFT(-10,000)~ DSBPAN_RIGHT(+10,000)
);
IDirectSoundBuffer8 接口的其他方法
IDirectSoundBuffer8 接口的其他方法有:
Initialize——初始化未被初始化的声音缓冲区,使用 IDirectSound8 接口的
CreateSoundBuffer 方法所创建的声音缓冲区已被初始化了
GetCaps——获取声音缓冲区对象的性能
GetStatus——获取声音缓冲区对象的状态
GetFormat——获取声音缓冲区的波形格式
GetFrequency——获取采样频率
SetFrequency——设置采样频率
GetPan——获取左右声道的音量
GetVolume——获取声音的衰减量(0~10000),单位为百分之一分贝(dB%)
GetCurrentPosition——获取播放和写入点(cursor)的位置
SetCurrentPosition——设置播放点(cursor)的位置
AcquireResources——为缓冲区分配资源
Restore——为丢失的声音缓冲区恢复分配的存储区
GetObjectInPath——为伴随缓冲区的音效对象获取接口
SetFX——启动缓冲区上的音效
从父接口 IUnknown 继承下来的基类方法有:
AddRef——添加引用
QueryInterface——查询接口
Release——释放缓冲区对象
例子
下面的代码段,先设置波形格式并创建辅缓冲区对象,然后用计算出的 440Hz(标准
A)纯音的正弦波形音频数据来填充辅缓冲区,最后播辅放缓冲区(并停止播放辅缓冲区和
释放辅缓冲区):
#define _USE_MATH_DEFINES
#include <cmath> // 为了使用 M_PI 和数学函数
// 设置波形格式
WAVEFORMATEX wfx; // 定义波形格式结构变量
ZeroMemory( &wfx, sizeof(WAVEFORMATEX) ); // 清零
= WAVE_FORMAT_PCM; // PCM 格式
= 1; // 单声道
= 44100; // 采样频率
= 16; // 16 位样本
= (WORD) ( / 8 * ); // 块对齐
= * ; // 平均传输率
// 设置缓冲区描述结构
DWORD secs = 1; // 声音长度(秒数)
DSBUFFERDESC dsbd; // 定义缓冲区描述结构变量
ZeroMemory( &dsbd, sizeof(DSBUFFERDESC) ); // 清零
= sizeof(DSBUFFERDESC); // 结构大小
= DSBCAPS_STATIC; // 静态缓冲区
= * / 8
* * secs; // 缓冲区字节大小
= GUID_NULL; // 无双扬声器虚拟化算法
= &wfx; // 要设置的波形格式结构
// 创建辅缓冲区
LPDIRECTSOUNDBUFFER pBuffer;
if( g_pDS->CreateSoundBuffer( &dsbd, &pBuffer, NULL ) == DS_OK ) {
// 写入波形音频数据
unsigned char *pBufferBytes; // 定义缓冲区指针
DWORD lockedsize; // 定义锁定块大小变量
if( pBuffer->Lock(0, , (void **)(&pBufferBytes), &lockedsize,
NULL, NULL, 0L) == DS_OK ) { // 锁定缓冲区
float frequency = ; // 标准音 A 的频率
for (DWORD q=0; q < lockedsize; q++) { // 计算并填充数据
float pos = frequency / (float) * (float)q; // 周期数
float r = (pos - floor(pos)) * 2 * (float)M_PI; // 转换为角度
float value = sin(r); // 正弦音频数据(-1 ~ 1)
pBufferBytes[q] = 127 + (BYTE)(value * 127); // 改变振幅(0~255)
}
pBuffer->Unlock( pBufferBytes, lockedsize, NULL, 0 ); // 解锁
pBuffer->Play(0, 0, DSBPLAY_LOOPING); // 播放缓冲区
//pBuffer->Stop(); // 停止播放缓冲区
//pBuffer->Release(); // 释放缓冲区
}
}
编程步骤及方法
现在将上面两节所介绍的 DirectSound 编程的具体步骤及方法汇总到图 13-27 中。
图 13-27 DirectSound 编程的基本步骤及方法
播放 WAV 文件和资源
创建辅声音缓冲区后,即可以像上面的例子所示的那样,用算法生成波形音频数据来填
充缓冲区;也可以将读入的 WAV 文件或资源中的音频数据,直接写入辅声音缓冲区以供播
放。即,可以用 DirectSound 来播放现存的 WAV 文件和资源。
为了播放 WAV 文件和资源,在创建对应辅声音缓冲区之前,必须先知道 WAV 文件或
资源的波形音频格式和音频数据的大小;这可以通过调用 小节中给出的 GetWave 函数
(它会再调用 OpenWave 和 GetWaveData)来获取。在创建对应辅声音缓冲区之后,可以利
用 GetWave 函数所返回的文件句柄,来直接调用 MMIO 的 mmioRead 函数,将 WAV 文件
调用全局函数 DirectSoundCreate8
来生成 DirectSound 接口对象
调用 DirectSound 接口的
SetCooperativeLevel 方法来
设置应用程序对声音设备的合作级别
调用 DirectSoundBuffer 接口的 SetFormat
方法来设置主声音缓冲区的波形音频格式
调用 DirectSound 接口的
CreateSoundBuffer 方法来创建辅声音缓冲区
调用 DirectSound 接口的
CreateSoundBuffer 方法来获取主声音缓冲区
调用 DirectSoundBuffer8 接口的
Lock 方法来锁定辅声音缓冲区
并返回音频数据存储区的地址指针
写入(生成或装入)音频数据
调用 DirectSoundBuffer8 接口的
Unlock 方法来解锁辅声音缓冲区 调用 DirectSoundBuffer8 接口的 GetStatus
方法来判断缓冲区对应的存储区是否被丢失
调用 DirectSoundBuffer8 接口的
Restore 方法来恢复声音缓冲区
调用 DirectSoundBuffer8 接口的
Play 方法来播放辅声音缓冲区
调用 DirectSoundBuffer8 接口的
Stop 方法来播放辅声音缓冲区
调用父接口的 Release 方法
来释放各接口对象所占用的内存
调用 DirectSoundBuffer8 接口的
SetPan 方法来设置左右声道的相对音量
调用 DirectSoundBuffer8 接口的
SetVolume 方法来设置声音的衰减量
或资源中的波形数据,填入辅声音缓冲区。最后通过调用缓冲区接口对象的 Play 方法,来播
放对应的 WAV 文件和资源。
下面是播放 WAV 文件和资源的函数代码:(/* 资源代码 */)
// 播放 WAV 文件(或资源)
bool PlayWave(LPWSTR strFilePath /* UINT uiResID */ ) {
// 打开波形文件(或资源)
HMMIO hmmio = mmioOpen(strFilePath, NULL, MMIO_READ);
/* = OpenWave(uiResID); */
if ( hmmio == NULL ) {
MessageBox(L"Open WAV file error!", L"Error");
return false;
}
// 获取波形数据
WAVEFORMATEX wfFormat;
DWORD dwDataLen;
if ( !GetWaveData(hmmio, wfFormat, dwDataLen) ) {
MessageBox(L"Get wave data error!", L"Error");
return false;
}
// 创建 DS 对象
LPDIRECTSOUND8 pDirectSound;
if (DirectSoundCreate8(NULL, &pDirectSound, NULL) != DS_OK) {
MessageBox(L"Could not create DirectSound object!", L"Error");
return false;
}
// 设置 DS 对象的合作级别
if (pDirectSound->SetCooperativeLevel(GetSafeHwnd(),
DSSCL_NORMAL) != DS_OK) {
MessageBox(L"Could not set cooperative level!", L"Error");
pDirectSound->Release();
return false;
}
// 设置 DS 辅缓冲区描述结构
DSBUFFERDESC dsbdDesc;
ZeroMemory(&dsbdDesc, sizeof(DSBUFFERDESC));
= sizeof(DSBUFFERDESC);
= DSBCAPS_STATIC;
= dwDataLen;
= &wfFormat;
// 创建 DS 辅缓冲区对象
LPDIRECTSOUNDBUFFER pDSBuffer;
if ( pDirectSound->CreateSoundBuffer(&dsbdDesc, &pDSBuffer, NULL)
!= DS_OK ) {
MessageBox(L"Create sound buffer error!", L"Error");
pDirectSound->Release();
return false;
}
// 锁定 DS 辅缓冲区
BYTE* pDSBuffData;
if ( pDSBuffer->Lock(0, dwDataLen, (LPVOID*)&pDSBuffData, &dwDataLen,
NULL, 0, 0) != DS_OK ) {
MessageBox(L"Lock sound buffer error!", L"Error");
pDirectSound->Release();
return false;
}
// 向 DS 辅缓冲区中写入波形数据
if ( mmioRead(hmmio, (HPSTR)pDSBuffData, dwDataLen) < dwDataLen ) {
MessageBox(L"Read data from WAV file error!", L"Error");
pDirectSound->Release();
return false;
}
// 解锁 DS 辅缓冲区
if ( pDSBuffer->Unlock(pDSBuffData, dwDataLen, NULL, 0) != DS_OK ) {
MessageBox(L"Unlock sound buffer error!", L"Error");
pDirectSound->Release();
return false;
}
pDSBuffer->Play(0, 0, 0); // 播放 DS 辅缓冲区
//pDSBuffer->Stop(); // 停止播放 DS 辅缓冲区
//pDSBuffer->Release(); // 释放 DS 辅缓冲区
//pDirectSound->Release(); // 释放 DS 对象
return true;
}
其中,用到了 小节中给出的 OpenWave(、GetWaveData)和 GetWave 等函数。还使
用了 CWnd 的成员函数(返回 m_hWnd):HWND GetSafeHwnd( ) const;。
混音播放实例
本小节结合一个战争声效的例子,介绍如何利用 DirectSound 所提供的音频功能,来重
叠播放和控制多个音频段,这在游戏和其他许多多媒体程序中都是必需和常见的。
我们采用随机声道混音,来模仿战场上枪林弹雨和爆炸声的音响效果。为此创建一个没
有文档支持、没有工具条和状态条的 SDI 应用程序项目 War。在项目属性中添加对多媒体库
与 DirectSound 库( )的链接。
添加 WAV 资源
用资源编辑器顺序依次将 13 个音响波形文件加入项目,修改其 ID 为:
IDR_HELICOPTER WAVE DISCARDABLE "Waves\\" // 直升机声
IDR_RICOCHET1 WAVE DISCARDABLE "Waves\\" // 飞弹声
IDR_RICOCHET2 WAVE DISCARDABLE "Waves\\"
IDR_GUNFIRE1 WAVE DISCARDABLE "Waves\\" // 枪声
IDR_GUNFIRE2 WAVE DISCARDABLE "Waves\\"
IDR_GUNFIRE3 WAVE DISCARDABLE "Waves\\"
IDR_GUNFIRE4 WAVE DISCARDABLE "Waves\\"
IDR_GUNFIRE5 WAVE DISCARDABLE "Waves\\"
IDR_EXPLODE1 WAVE DISCARDABLE "Waves\\" // 爆炸声
IDR_EXPLODE2 WAVE DISCARDABLE "Waves\\"
IDR_EXPLODE3 WAVE DISCARDABLE "Waves\\"
IDR_EXPLODE4 WAVE DISCARDABLE "Waves\\"
IDR_SIREN WAVE DISCARDABLE "Waves\\" // 警报声
注意,要使其对应的 ID 常数连续(在头文件 中),例如:
#define IDR_HELICOPTER 130
#define IDR_RICOCHET1 131
#define IDR_RICOCHET2 132
#define IDR_GUNFIRE1 133
#define IDR_GUNFIRE2 134
#define IDR_GUNFIRE3 135
#define IDR_GUNFIRE4 136
#define IDR_GUNFIRE5 137
#define IDR_EXPLODE1 138
#define IDR_EXPLODE2 139
#define IDR_EXPLODE3 140
#define IDR_EXPLODE4 141
#define IDR_SIREN 142
头文件
在视图类的头文件中包含头多媒体和 DirectSound 的头文件、定义常量、成员变量和成
员函数的原型。
在 CChildView 类的头文件中包含头文件:
#include <> // 多媒体
#include <> // DirectSound
定义宏常量:(作为循环上限)
#define NUMSOUNDS 13
在 CChildView 类中定义若干成员变量:
LPDIRECTSOUND8 m_pDirectSound; // DirectSound 对象指针
LPDIRECTSOUNDBUFFER m_pDSBuffer[NUMSOUNDS]; // DirectSound 对象指针数组
LONG m_lHeliPan; // 直升机移音,用于左右移动其声音
LONG m_lHeliPanInc; // 直升机移音增量
和成员函数的原型:
bool InitDirectSound(); // 初始化 DirectSound
void CleanupDirectSound(); // 清除 DirectSound
bool InitDSBuffers(); // 初始化 DirectSoundBuffer
bool RestoreDSBuffers(); // 恢复 DirectSoundBuffer
其中包括若干用于读取 WAV 文件和资源的 MMIO 函数:
HMMIO OpenWave(LPWSTR strFilePath); // 打开波形文件
HMMIO OpenWave(UINT uiResID); // 打开波形资源
bool GetWaveData(HMMIO hmmio, WAVEFORMATEX& wfFormat, DWORD&
dwDataLen); // 获取波形数据
bool GetWave(LPWSTR strFilePath, HMMIO& hmmio, WAVEFORMATEX&
wfFormat, DWORD& dwDataLen); // 获取波形文件数据
bool GetWave(UINT uiResID, HMMIO& hmmio, WAVEFORMATEX& wfFormat,
DWORD& dwDataLen); // 获取波形资源数据
代码文件
在视图类的代码文件中,添加初始化和清除代码、窗口创建和计时器消息响应函数,实
现各个成员函数。
// 初始化指针和变量
CChildView::CChildView() {
m_pDirectSound = NULL;
m_lHeliPan =0;
m_lHeliPanInc = 0;
for (int i = 0; i < NUMSOUNDS; i++) m_pDSBuffer[i] = NULL;
}
// 清除声音对象,删除计时器
CChildView::~CChildView() {
CleanupDirectSound();
KillTimer(1);
}
// 清除声音对象
void CChildView::CleanupDirectSound() {
// Cleanup the DS object
if (m_pDirectSound) {
m_pDirectSound->Release();
m_pDirectSound = NULL;
}
}
将 小节中 MMIO 的 5 个读取波形文件和资源的函数:OpenWave(2 个:文件、
资源)、GetWaveData(1 个:文件/资源)和 GetWave(2 个:文件、资源),添加为视图类
的 CChildView 成员函数。
下面 InitDirectSound 成员函数,用来初始化声音对象。具体步骤有:创建声音对象、设
置合作水平、调用 InitDSBuffers 成员函数来初始化声音缓冲区。
// 初始化声音对象
bool CChildView::InitDirectSound() {
// 创建 DS 对象
if (DirectSoundCreate8(NULL, &m_pDirectSound, NULL) != DS_OK) {
MessageBox(L"Could not create DirectSound object!",
L"DirectSound Error");
CleanupDirectSound();
return false;
}
// 设置 DS 对象的合作级别
if (m_pDirectSound->SetCooperativeLevel(GetSafeHwnd(),
DSSCL_NORMAL) != DS_OK) {
MessageBox(L"Could not set cooperative level!", L"DirectSound Error");
CleanupDirectSound();
return false;
}
// 初始化 DS 缓冲区
if (!InitDSBuffers()) {
MessageBox(L"Could not initialize DirectSound buffers!",
L"DirectSound Error");
CleanupDirectSound();
return false;
}
return true;
}
下面 InitDSBuffers 成员函数,用来初始化声音缓冲区。具体做法是:用 for 循环来初始
化 13 个与 WAV 文件或资源对应的辅声音缓冲区数组;其中调用了自己编写的(利用 MMIO
读取波形文件和资源的)GetWave 函数,来获得波形音频的结构和波形数据的大小;还调用
了 MMIO 的 mmioRead 函数,来将 WAV 文件或资源中的波形数据,填入辅声音缓冲区;这
里所创建的辅缓冲区类型为:
DSBCAPS_CTRLPAN——可控制面板,即可调节左右声道的音量
DSBCAPS_CTRLVOLUME——可控制音量
DSBCAPS_CTRLFREQUENCY——可控制频率
DSBCAPS_STATIC——静态缓冲区
// 初始化声音缓冲区
bool CChildView::InitDSBuffers() {
/*CString fps[] = {L"Waves\\", // 直升机声
L"Waves\\", // 飞弹声
L"Waves\\",
L"Waves\\", // 枪声
L"Waves\\",
L"Waves\\",
L"Waves\\",
L"Waves\\",
L"Waves\\", // 爆炸声
L"Waves\\",
L"Waves\\",
L"Waves\\",
L"Waves\\"}; // 警报声*/
// 初始化波形和 DS 缓冲区
for (int i = 0; i < NUMSOUNDS; i++) {
// 获取波形信息
HMMIO hmmio;
WAVEFORMATEX wfFormat;
DWORD dwDataLen;
/*if ( !GetWave((LPWSTR)fps[i].AllocSysString(), hmmio, wfFormat, dwDataLen) )*/
if ( !GetWave(IDR_HELICOPTER + i, hmmio, wfFormat, dwDataLen) )
return false;
// 设置 DS 缓冲区描述
DSBUFFERDESC dsbdDesc;
ZeroMemory(&dsbdDesc, sizeof(DSBUFFERDESC));
= sizeof(DSBUFFERDESC);
= DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME |
DSBCAPS_CTRLFREQUENCY | DSBCAPS_STATIC;
= dwDataLen;
= &wfFormat;
// 创建 DS 缓冲区
if ( m_pDirectSound->CreateSoundBuffer(&dsbdDesc, &m_pDSBuffer[i], NULL)
!= DS_OK ) return false;
// 锁定 DS 缓冲区
BYTE* pDSBuffData;
if ( m_pDSBuffer[i]->Lock(0, dwDataLen, (LPVOID*)&pDSBuffData,
&dwDataLen, NULL, 0, 0) != DS_OK ) return false;
// 将波形数据写入 DS 缓冲区
dwDataLen = mmioRead(hmmio, (HPSTR)pDSBuffData, dwDataLen);
// 解锁 DS 缓冲区
if ( m_pDSBuffer[i]->Unlock(pDSBuffData, dwDataLen, NULL, 0) != DS_OK )
return false;
}
return true;
}
其中,用 /*……*/ 注释掉的代码段,是用于打开 WAV 文件的,可以替换掉打开 WAV 资源
的语句:if ( !GetWave(IDR_HELICOPTER + i, hmmio, wfFormat, dwDataLen) ) return false;
因为系统有时会释放与声音缓冲区关联的存储区,如果这时播放该缓冲区,则会产生意
想不到的结果。解决办法是,在每次播放缓冲区前,调用声音缓冲区对象的 GetStatus 成员函
数,来判断缓冲区对应的存储区是否被丢失,如果已经被丢失,可调用声音缓冲区对象的
Restore 成员函数来恢复缓冲区。下面的 RestoreDSBuffers 函数,就是用于恢复 13 个声音缓
冲区的。
bool CChildView::RestoreDSBuffers() { // 恢复声音缓冲区
for (int i = 0; i < NUMSOUNDS; i++)
if (m_pDSBuffer[i]->Restore() != DS_OK) return false;
return InitDSBuffers(); // 重新初始化缓冲区
}
为了在程序启动时就播放战争音效,可以为视图类 CChildView 的窗口创建信息
WM_CREATE,添加消息响应函数 OnCreate。在该函数中,初始化声音对象和伪随机数种子,
设置计时器。
// 窗口创建消息响应函数
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if (CWnd::OnCreate(lpCreateStruct) == -1) return -1;
if (!InitDirectSound()) return -1;
srand( (UINT)timeGetTime() ); // 产生随机数种子
if (!SetTimer(1, 500, NULL)) return -1; // 设置计时器
return 0;
}
其中:
void srand( // 设置随机数的起点
unsigned int seed // 随机数生成器的种子
);
DWORD timeGetTime(void); // 获取系统时间(从系统启动到现在的毫秒数)
还要为视图类 CChildView,添加计时器消息 WM_TIMER 的响应函数 OnTimer。在该函
数中,随机播放各种战争音效的 WAV 文件或资源。并调节左右声道的相对音量,以产生声
音移动的立体声效果。对直升飞机的声音,采用循环播放,并不断调节左右声道的相对音量,
还依据其离中心平衡点的远近,来设置主音量的大小,越远声音越小。
// 计时器消息响应函数
void CChildView::OnTimer(UINT_PTR nIDEvent) {
if (::rand() % 2) { // 50%的概率来播放声音
int nIndex = ::rand() % NUMSOUNDS; // 随机确定播放哪一种声音
if (m_pDSBuffer[nIndex] != NULL) {
DWORD dwStatus;
m_pDSBuffer[nIndex]->GetStatus(&dwStatus);
// 查看缓冲区是否被丢失
if (dwStatus & DSBSTATUS_BUFFERLOST) RestoreDSBuffers();
// 查看该声音是否已经在播放
if (!(dwStatus & DSBSTATUS_PLAYING)) {
if (nIndex > 0) { // 非直升飞机声
// 随机设置声音的左右音量(空间定位)
m_pDSBuffer[nIndex]->SetPan((::rand() % 2000) - 1000);
// 设置声音的音量
m_pDSBuffer[nIndex]->SetVolume((::rand() % 3) * -250);
m_pDSBuffer[nIndex]->Play(0, 0, 0); // 播放声音
}
else { // 直升飞机声
// 设置直升飞机的左右声道的相对音量
LONG lStart = (::rand() % 2) ? -1 : 1; // 起始方向:左或右
m_lHeliPan = lStart * 8000; // 起始相对音量
m_lHeliPanInc = -(lStart * 400); // 相对音量的增量
m_pDSBuffer[nIndex]->SetPan(m_lHeliPan); // 设置相对音量
// 循环播放直升飞机声
m_pDSBuffer[nIndex]->Play(0, 0, DSBPLAY_LOOPING);
}
}
}
}
// 如果正在播放直升飞机声,看是否需要停止它
DWORD dwStatus;
m_pDSBuffer[0]->GetStatus(&dwStatus);
if (dwStatus & DSBSTATUS_LOOPING) {
if ((m_lHeliPan < -8000) || (m_lHeliPan > 8000)) m_pDSBuffer[0]->Stop();
else {
m_lHeliPan += m_lHeliPanInc;
m_pDSBuffer[0]->SetPan(m_lHeliPan); // 移动声音的位置
m_pDSBuffer[0]->SetVolume(-(::abs(m_lHeliPan) / 5)); // 远离时减小
}
}
CWnd::OnTimer(nIDEvent);
}
最后可以在 CChildView 类的 OnPaint 消息响应函数中,添加几行代码,在窗口中写上一
行字"闭上你的眼睛,聆听战争...":
void CChildView::OnPaint() {
CPaintDC dc(this); // 用于绘制的设备上下文
CFont font;
(420, L"黑体");
(font);
(RGB(255, 0, 0));
(10, 200, L"闭上你的眼睛,
聆听战争...");
图 13-28 War 程序的界面
}
运行结果的界面见图 13-28。
复习思考题
1. 为什么已经有了 MCI 和 MMAPI 微软还要开发 DirectX?
2. 给出 DirectX 的字面意思和功能特点。
3. DirectX 有哪些组件模块?它们的功能各是什么?
4. DirectX 的第一版是什么时候推出的?当前的最新版本是多少?给出其组件模块随
版本的演化情况。
5. OpenML 的含义和功能是什么?它是谁开发的?为什么要开发它?
6. 给出 DirectX 结构体系中的 HAL 与 HEL 的含义与用处。微软公司现在减少了对其
中那个部分的支持?影响如何?为什么?
7. 进行 DirectX 编程前,需要做哪些准备工作?
8. 给出 Direct3D 与 DirectDraw 的关系。
9. 给出 Direct3D 的流水线和设备的功能与组成和架构。
10. 如何创建和初始化 Direct3D 及其设备对象?
11. Direct3D 中的三维场景是由什么构成的?如何创建场景中的几何对象?
12. Direct3D 在对场景进行渲染前,需要进行哪些矩阵变换?各变换的功能又是什么?
13. Direct3D 中的光照和灯光有哪些模式和类型?它们各有什么特性?
14. 在 Direct3D 中,材质起什么作用?如何描述和设置物体的材质?
15. 在 Direct3D 中,渲染的功能和作用是什么?渲染和显示场景的步骤有哪些?
16. DirectX 的音频包含哪些组件模块?它们各有什么特点和功用?
17. DirectSound 有哪些功能和主要接口对象?
18. DirectSound 设备对象对应于什么?如何创建 DirectSound 对象?DirectSound 对象
的主要功能是什么?
19. DirectSoundBuffer 对象表示什么?它有哪两种类型?它们的含义和功用各是什么?
20. 如何创建 DirectSoundBuffer 对象?如何写入音频数据?怎样播放声音?
21. 如何利用 DirectSound 来播放 WAV 文件和资源?
22. 在 DirectSound 中如何设置音量?如何调整声音的立体声定位?
23. 给出混音播放实例——战争音效的编写思路与关键步骤。
作业
平时作业 27:实现旋转圆筒的 Direct3D 例子(材质与光照 或/和 纹理)。
平时作业 28:实现战争声效程序 War。
大作业选题 19:DirectX 的研究、编程与应用。
大作业选题 20:带音效 3D 游戏的设计与实现。
大作业选题 21:DirectX 的 Direct3D+DirectX Audio 与 OpenGL+OpenML 的比较研究。