GDI 魔术 - 渲染反射和阴影
一篇关于使用 Windows GDI 渲染反射和阴影的文章
引言
本文介绍了一种仅使用 Windows GDI 渲染 3D 对象反射和流畅阴影的简单方法。屏幕上没有真正的 3D 对象。我们只使用位图以及对 BitBlt()
和 PlgBlt()
Win32 API 函数的一些调用。
背景
我一直很喜欢人们如何利用 CPU 功耗来模拟现实世界中物体的行为。在当今的 3D 游戏中,我看到过整个虚拟世界仅为一个目的而创建——为了娱乐和玩耍。由于我不是 3D 建模专家,但我希望创建一个在场景中看起来逼真的东西,我使用了当时唯一拥有的武器:Windows GDI。我构建了一个演示场景,以向那些想知道如何做这类事情的人展示。这比看起来容易,但创建起来花费了一些时间。以下是实现方法……
场景背景
场景的背景是一个带有纹理的平面多边形。使用简单的 PlgBlt()
方法调用即可轻松完成。它接受目标 HDC
、多边形顶点、使用简单的 LoadBitmap()
方法调用加载的源位图的 HDC
、加载位图的偏移量和尺寸,以及一些设置为 NULL
的掩码参数。请参阅下面的代码。
// Drawing textured polygon
CBitmap bgBitmap;
bgBitmap.LoadBitmap(IDB_BITMAP_BACKGROUND);
BITMAP bmp;
bgBitmap.GetBitmap(&bmp);
POINT pBgBitmapPoints[3] = {{50, 250}, {380, 250}, {200, 400}};
PlqBlt(hDestDC, pBgBitmapPoints, hSrcDC, 0, 0, bmp.bmWidth, bmp.bmHeight, NULL, 0, 0);
确切的源代码可以在演示项目中的 DrawBackground()
方法中找到。
对象反射
现在是最有趣的部分:我们如何渲染对象的反射,使其所站立的表面看起来光亮光滑(像玻璃表面)?解决方案:我们将在该表面上渲染相同的对象,但会使用一个小技巧。首先,我们仅渲染可以产生反射的对象侧面。其次,我们在渲染这些侧面时使用“递减 alpha 混合”技术,以获得逼真的效果。算法如下:
对于每个有反射的侧面:
- 使用
BitBlt()
方法将被此侧面覆盖的屏幕部分绘制到第一个位图上。 - 使用
PlgBlt()
方法将反转的侧面绘制到第二个位图上。 - 沿着每一行移动时,使用递减的 alpha 混合技术合并这两个位图。
- 结果现在在第二个位图中。
- 再次使用
BitBlt()
方法将第二个位图绘制到屏幕上。
确切的源代码可以在演示项目中的 DrawReflectedBox()
方法中找到。
对象阴影
阴影的渲染可以简单也可以复杂——这完全取决于您。我决定使用一种称为“使用 z=0 平面上的地面变换渲染阴影”的简单技术,并且我使其更加简单。在此示例中,我没有使用精确的数学计算,因为这不是很重要,但即使如此,阴影看起来仍然很逼真。光源在无穷远处,因此光线是平行的。这意味着对象在地面(z=0 平面)上的投影保留了对象本身的尺寸。知道这一点后,我做了以下操作:
- 使用
BitBlt()
方法将此阴影覆盖的屏幕部分绘制到第一个位图上。 - 使用
BitBlt()
方法将此阴影覆盖的屏幕部分绘制到第二个位图上。 - 使用简单的模糊过滤技术过滤第二个位图(过滤器大小由您决定)。
- 沿着每一行移动时,使用恒定的 alpha 混合技术合并这两个位图。
- 结果现在在第二个位图中。
- 再次使用
BitBlt()
方法将第二个位图绘制到屏幕上。
确切的源代码可以在演示项目中的 DrawShadow()
方法中找到。
主对象
主对象与场景背景的渲染方式类似。它是一个带有 3 个侧面(多边形)的盒子,每个侧面都有不同的纹理。它在最后渲染,作为场景中的最后一个对象。
确切的源代码可以在演示项目中的 DrawOriginalBox()
方法中找到。
关于渲染速度的问题
请理解,此演示只是一个简单的示例,它不会试图打破 GDI 的任何速度记录。它只是说明有些事情可以做到,而不是说它们应该以这种方式完成。请参阅 DirectX/OpenGL 文档,了解在 Windows 平台上进行高质量 3D 图形渲染。
关注点
通过完成此示例,我了解到可以通过仅使用简单的 Win32 API 调用进行 GDI 渲染来创建高质量的现实场景。因此,有一些事情可以做到(并且看起来不错),而 GDI 本身并没有默认支持任何 DirectX/OpenGL 高级渲染模式:抗锯齿、阴影、反射等。