使用 Direct3D 8 飞越 Munsell 色彩立体






4.91/5 (29投票s)
2004 年 7 月 19 日
12分钟阅读

232388

5402
关于 Direct3D 8 的教程。
引言
Direct3D 是 DirectX 组件,可以渲染沉浸式 3D 世界。Direct3D 非常适合允许操作员在 3D 空间中穿梭复杂模型的程序。我处理的特定模型是 Munsell 色彩实体模型。Munsell 色彩实体是人眼可感知色彩范围的模型。它于 1905 年由 Albert H. Munsell 开发。之所以称为色彩实体,是因为颜色在模型的体积内连续变化。通常,您会看到 Munsell 色彩实体在 2D 空间中显示,方式如下:
穿过 Munsell 色彩实体的垂直切片会得到如下 2D 图像

最暗、最不鲜艳的颜色位于 Munsell 色彩实体的中心附近。实际上,中心轴是称为 灰度的黑白颜色范围。模型的外部轮廓是凹凸不平的,这只是因为 Munsell 决定这样分配颜色。Munsell 是一位艺术家。大多数科学家会偏爱一种显示某种形式的几何对称性的颜色模型,例如球体或金字塔。但没有理由相信更清晰的几何形状能更好地描绘人类的颜色感知。
DirectX 背景知识
DirectX 是微软的一项技术,它允许开发人员编写能够实现高帧率的计算机游戏。与微软的原始技术 GDI(“图形设备接口”)相比,DirectX 允许您的程序更快地写入显示器。Direct3D 是 DirectX 的一部分,可以绘制具有平滑 Gouraud 着色和科学准确照明的三维形状。借助 Direct3D,我们可以创建一个程序,允许操作员动态调整其相对于 3D 对象的位置,进行缩放,以及围绕其中心轴旋转对象。您甚至可以穿过对象内部进行查看。
我决定采用 Direct3D 8,这是 Windows XP 操作系统自带的版本。微软此后已转向 9.0 版本。DirectX 9 版本可以安装在 Windows 98、Windows Me、Windows 2k 和 Windows XP 上。
要开发 Direct3D 程序,您需要从微软获取 DirectX SDK(软件开发工具包)。这超过 200 MB,下载起来很麻烦(微软以前提供 10 美元的 CD-ROM SDK,但他们似乎已经停止了)。
要仅执行(而不是构建)使用 Direct3D 的程序,您只需要小得多的 DirectX 运行时,您也可以从微软下载。Windows XP 包含此运行时,而早期的微软操作系统则不包含。或者,大多数电脑游戏会安装某个版本的 DirectX 运行时,因此如果您购买并安装了相当近期的游戏,您的计算机可能已经拥有 8.0 或 9.0 运行时。安装 DirectX 9 运行时后,您应该能够执行任何使用 DirectX 9、DirectX 8、DirectX 7 等的程序。
请注意,一旦您安装了任何版本的 DirectX 技术,您将无法轻松将其从计算机中卸载。微软不提供卸载功能。因此,微软建议 Windows Me 和 Windows XP 用户在安装新版本的 DirectX 之前创建“系统还原”点。Windows 9x 和 Windows 2k 用户将需要重新安装操作系统才能恢复到早期版本的 DirectX。
我的程序除了 DirectX 之外没有任何其他依赖项。它使用了 Charles Petzold 的著名书籍“Programming Windows”中描述的标准 WIN32 API。我没有使用 MFC、ATL、STL、WTL 等。
代码
我的目标是使用 Direct3D 显示 Munsell 色彩实体的 20 个径向切片,并允许操作员从任意旋转、倾斜和缩放因子查看此色彩实体表示。操作员可以使用 r 和 R 键旋转模型,e 和 E 键将其视角提升到中心线之上或之下,z 和 Z 键选择观看距离。下面显示了完成后的程序呈现的典型视图。

当程序工作时,我意识到它是 透明纹理 和 alpha 测试 重要性的绝佳例证。
纹理只是一个 .BMP 文件,用于为 3D 模型提供表面着色。这个程序使用了 20 个 .BMP 文件,其中包含 Munsell 色彩实体的径向切片。下面显示了其中一个 .BMP 文件。

您可以看到这个 .BMP 文件有一个中灰色背景。微软的 D3DXCreateTextureFromResourceEx()
函数提供了一种将特定颜色声明为 颜色键 的方法,这意味着该颜色不会被复制到屏幕上。在 Direct3D8 中实现这一点的方法是通过启用 alpha 混合,例如:
pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true );
然后,颜色键颜色将在纹理中出现的任何地方都变为透明。在运行程序时,您可以按 't' 键切换中灰色是否被视为透明。当颜色不被视为透明时,Munsell 色彩实体看起来会像这样

显然,这很混乱。但即使在成功处理了透明纹理之后,我仍然观察到了以下较小的混乱情况。

您确实需要下载 .EXE 可执行文件(或从我提供的 Visual C++ 6 项目自行构建),然后亲自旋转模型以观察上述图像失真动态特征。
起初,似乎在这个最后一个图像中可见的失真仅发生在 Munsell 色彩实体的左侧。碰巧,您正在查看 Munsell 色彩模型那一侧的 Direct3D 表面的 背面。您可以通过键入 'b' 键来切换 Direct3D 的 背面剔除 模式来验证这一点。如果失真仅存在于色彩实体的左侧,那么问题可能就是透明纹理与背面之间的交互。
但这只是一个红鲱鱼,因为当我发现只要我稍微旋转模型,就会在 Munsell 色彩实体的右侧出现类似的图像失真时,我就明白了。您可以在下面的图片中看到这一点。

我意识到我看到的是我算法绘制到屏幕上的第一个径向切片的灰色“透明”部分。Munsell 色彩实体的右侧的所有其他径向切片看起来都很好,除了这一个特殊的切片,而这个有问题的切片是我绘制的第一个切片。
我通过采用更复杂的渲染算法解决了这个问题并消除了视觉失真。我的原始程序总是先绘制切片 #0,然后是切片 #1,依此类推,而不管模型的当前旋转。当我费心按照模型当前旋转决定的顺序绘制径向切片时,视觉失真就消失了。只要切片按从后到前的顺序渲染,最终图像就是完美的。您可以使用 'p' 键在原始算法和更智能的对切片进行排序的算法之间切换,从而看到其中的区别。我选择 'p' 键是因为这通常被称为 画家算法,这意味着油画家最后放在画布上的颜色就是您看到的颜色。
在我将此代码发布到我的网站 www.computersciencelab.com/Direct3DTut1.htm 后,我从瑞典的 Henrik Rydgård 那里得知了一个更简单的解决方案。如果特定显卡支持 alpha 测试,那么即使程序继续使用始终从切片 #0 开始绘制的原始算法,也可以避免视觉失真。
要理解 alpha 测试,您首先需要理解导致视觉失真的机制。假设切片 #0 是红色切片(事实确实如此),并且模型的当前旋转意味着您应该能看到一些粉红色切片出现在红色切片后面(再次查看最后一张图片)。您需要在屏幕上那些通常会绘制红色切片灰色“透明”边框的区域看到粉红色切片。您可以通过将灰色声明为 颜色键 来防止构成切片 #0 纹理边框的灰色像素被复制到显示表面。但是,尽管灰色像素未复制到显示表面,但它们在 3D 空间中的位置已被写入 深度缓冲区!!因此,当您稍后尝试创建需要出现在切片 #0 后面的切片 #1 的粉红色像素时,Direct3D 将不会绘制这些像素,因为它们到查看器的 z 距离(即它们的深度)比该显示点在 z 缓冲区(深度缓冲区)中记录的距离更远,导致 Direct3D 认为这些像素被更近的物体遮挡了。但那个物体是透明的,所以这不是我们想要的行为。
起初我以为 Direct3D 无法解决这个问题。但正如我从 Henrik 那里学到的,它确实提供了解决方案。您只需要添加以下语句:
pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, true ); pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0x01 ); pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL );
这 3 条语句要求 Direct3D 除非其 alpha 值超过某个值,否则不要将透明像素写入 z 缓冲区(深度缓冲区)。对于我的纹理,0x01 的特定值就足够了。如果切片 #0 的透明像素未写入深度缓冲区,那么稍后绘制的切片的(非透明)像素仍然可以被渲染并显示在先前绘制的切片的透明区域下方。添加这 3 条语句后,我就可以抛弃从后到前排序切片的复杂性,而是始终先绘制切片 #0,然后是切片 #1,依此类推。但同样,这在我自己的计算机上是可行的,因为我的显卡提供了此功能。您的显卡可能支持也可能不支持此特定 DirectX 功能。
您可以通过以下方式查询您的显卡是否具有此功能:
D3DCAPS8 d3dCaps; pd3dDevice->GetDeviceCaps( &d3dCaps ); if ( d3dCaps.AlphaCmpCaps & D3DPCMPCAPS_GREATEREQUAL ) { pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, true ); pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0x01 ); pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL ); } else { //<use the painter's algorithm> }
由于这种硬件依赖性,该程序最好按从后到前的顺序渲染切片,因为这样可以与所有显卡兼容。您可以在我的源代码中的适当位置找到 SetRenderState( D3DRS_ALPHATESTENABLE, true )
语句,但它被注释掉了。这样,您就可以使用 'p' 键在原始渲染算法(始终以切片 #0 开始)和更智能的从后到前算法(“画家算法”)之间切换,无论您拥有哪种显卡,都能看到其中的区别。
该程序是用 C++ 编写的,并且注释非常详尽。例如,这是我 cMunsell
类中的 SetRenderStates()
成员函数。
HRESULT cMunsell::SetRenderStates() { HRESULT hr; IDirect3DDevice8 * pd3dDevice = g_pd3dDevice; // The D3DRENDERSTATETYPE enumerated type is used with // IDirect3DDevice8::SetRenderState() to specify all possible // rendering states. // By default, Direct3D performs lighting calculations on all // vertices. And consequently, if a vertex has no normal vector // it will receive zero light. An application that specifies // vertex colors probably will not specify normal vectors and // hence must be sure to disable lighting or else everything // will be black. // If you plan to cover all surfaces with textures then you // probably want to disable Direct3D lighting. Otherwise, // you will use IDirect3DDevice8::SetLight() and // IDirect3DDevice8::SetMaterial(). hr = pd3dDevice->SetRenderState( D3DRS_LIGHTING, false ); if ( FAILED( hr ) ) return hr; // Depth buffering is a method of removing hidden lines and surfaces. // By default, Direct3D does not use depth buffering but it can // be enabled using the D3DRS_ZENABLE state. // Enable the z-buffer. hr = pd3dDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_TRUE ); if ( FAILED( hr ) ) return hr; // To convince Direct3D to render the inside of an object in // addition to its outside you will need to turn off back-face // culling. hr = pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ); if ( FAILED( hr ) ) return hr; // Since we want to employ color keying when we load our textures, // we need to enable alpha blending. hr = pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true ); if ( FAILED( hr ) ) return hr; // Alpha blending means combining the color value of the new pixel // with the pixel already stored at that location in the frame buffer. // There are a number of ways of doing this, described by the eq.: // FinalColor = TexelColor × SourceBlendFactor // + PixelColor × DestBlendFactor // To achieve complete transparency you would request: // hr = pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); // hr = pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); // But we want the alpha blending formula to vary on a pixel by pixel // basis, as described by the alpha channel of the source. This // is accomplished by: hr = pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); if ( FAILED( hr ) ) return hr; hr = pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); return hr; } // cMunsell::SetRenderStates()
编译
我包含了我的可执行文件(Munsell.exe),以便您无需自行编译代码即可进行实验。
如果您确实想编译代码,则需要先安装微软的 DirectX 9 SDK。微软提供免费下载。要从微软下载 DirectX,请访问网址:
然后在左侧列中点击“DirectX”。这将打开一个超链接列表,其中包括“最终用户运行时”(约 20MB)和一个“软件开发工具包”(或 SDK,超过 200MB)。这些超链接都会提到 DirectX 的当前版本(截至目前,是 DirectX 9.0b),并且该版本与此处描述的软件兼容。微软不再分发旧版本的 DirectX。再次强调,如果您运行的是 Windows XP 操作系统,您的计算机已经自带了 DirectX 8 运行时,这足以运行我的演示程序。但要自行编译代码,您必须安装完整的 SDK。
我包含了我的 Visual C++ 6 项目文件(Munsell.dsp)。我需要进行的唯一自定义操作是,将 d3d8.lib、d3dx8.lib 和 dxerr8.lib 库文件添加到“项目设置”对话框(通过选择 Visual C++ 6 菜单中的“项目/设置”出现)的“链接”选项卡下的“对象/库模块”列表中。此更改已包含在我提供的 Visual C++ 6 项目中,因此如果您创建新项目,才需要重复此操作。
由于 Visual C++ 6 比 DirectX 8 旧,您必须强制编译器使用 DirectX SDK 中的 DirectX .H 和 .LIB 文件,而不是 Visual C++ 6 附带的同名但更旧的文件。此自定义操作每个人都需要执行,因为此设置未存储在 .DSP 或 .DSW 项目文件中。选择 Visual C++ 6 菜单中的“工具/选项”,这将弹出“选项”对话框。在该对话框中激活“目录”选项卡,然后从标记为“显示目录用于”的组合框中选择“包含文件”。单击“新建”图标,然后输入您选择安装 DirectX SDK 的目录路径。假设您接受了 DirectX SDK 安装程序的默认设置,那么此目录路径将是:
- C:\mssdk\include
然后使用向上箭头图标将此新条目移到列表的顶部,以便在列表的其他成员之前进行搜索。
接下来,从标记为“显示目录用于”的组合框中选择“库文件”。单击“新建”图标,然后输入您选择安装 DirectX 8 SDK 的目录路径。假设您接受了 DirectX SDK 安装程序的默认设置,那么此目录路径将是:
- C:\mssdk\lib
然后使用向上箭头图标将此新条目移到列表的顶部。
历史
这是我第一次向 CodeProject 提交文章,我经常浏览这个网站。感谢所有做出贡献的人!您可以在 http://www.computersciencelab.com/ 上阅读更多关于我以及我其他软件项目的信息。