在 WPF 应用程序中使用 OpenGL
轻松在 WPF 应用程序中使用 OpenGL!

引言
在本文中,我将向您展示如何直接在 WPF 控件中渲染 OpenGL
,而无需任何窗口句柄或 WinFormsHost
对象等“技巧”。
我们将做的第一件事是创建一个执行某些 OpenGL
渲染的项目。在此之后,我将描述其内部工作原理 - 因此,如果您只想快速上手 OpenGL
,只需阅读文章的第一部分即可。
Beta 说明:本文使用的是 SharpGL 2.0 Beta 1 版本 - 这是一个 beta 版本,因此在完整版本可用时可能会稍有变动。
第一部分:WPF 中的 OpenGL 渲染
这将非常简单 - 第一件事是获取最新版本的 SharpGL
。SharpGL
是 OpenGL
库的 CLR 包装器 - 它支持硬件加速,并拥有从 OpenGL 1.1 到最新版本的 OpenGL
4.2 的所有核心功能和扩展。
从 CodePlex 下载页面获取核心二进制文件。这是下载页面,您需要核心二进制文件
或者,如果您愿意,也可以从文章顶部的链接下载核心二进制文件。
入门
创建一个新的 WPF 应用程序,命名为 WPFOpenGL
。现在,将下载的 SharpGL
和 SharpGL.WPF
程序集添加为引用。
SharpGL.dll 包含核心 OpenGL
功能。SharpGL.WPF
包含一个专门为您的 WPF 应用程序设计的控件。还有一个 SharpGL.WinForms
程序集,它为 Windows Forms 应用程序提供了一个类似的控件。
使用 OpenGLControl
在 MainWindow.xaml 文件的顶部,添加对 SharpGL.WPF
程序集的引用
<Window x:Class="WPFOpenGL.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sharpGL="clr-namespace:SharpGL.WPF;assembly=SharpGL.WPF"
Title="MainWindow" Height="350" Width="525">
<Grid>
</Grid>
</Window>
我们现在要做的就是在网格中添加一个 OpenGL
控件作为子元素
<Grid>
<sharpGL:OpenGLControl />
</Grid>
现在尝试运行应用程序 - 您至少会遇到一个错误,抱怨缺少对 System.Design
的引用。您必须确保将应用程序重新定位到 .NET Framework 4.0,而不是 .NET Framework 4.0 客户端配置文件。

重新定位应用程序后,它将正常运行,但不会显示任何内容!

这是因为我们还没有进行任何渲染。现在让我们继续。
使用 OpenGL 进行渲染
转到定义 OpenGL
控件的 XAML,输入 OpenGLDraw
,然后按两次 Tab 键 - 这将创建 OpenGL Draw
函数。
<sharpGL:OpenGLControl OpenGLDraw="OpenGLControl_OpenGLDraw" />
private void OpenGLControl_OpenGLDraw(object sender, SharpGL.OpenGLEventArgs args)
{
}
下面的代码段看起来有点长,但相当基础 - 我们只是提供金字塔和立方体的几何形状和颜色。
private void OpenGLControl_OpenGLDraw(object sender, SharpGL.OpenGLEventArgs args)
{
// Get the OpenGL instance that's been passed to us.
OpenGL gl = args.OpenGL;
// Clear the color and depth buffers.
gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT);
// Reset the modelview matrix.
gl.LoadIdentity();
// Move the geometry into a fairly central position.
gl.Translate(-1.5f, 0.0f, -6.0f);
// Draw a pyramid. First, rotate the modelview matrix.
gl.Rotate(rotatePyramid, 0.0f, 1.0f, 0.0f);
// Start drawing triangles.
gl.Begin(OpenGL.GL_TRIANGLES);
gl.Color(1.0f, 0.0f, 0.0f);
gl.Vertex(0.0f, 1.0f, 0.0f);
gl.Color(0.0f, 1.0f, 0.0f);
gl.Vertex(-1.0f, -1.0f, 1.0f);
gl.Color(0.0f, 0.0f, 1.0f);
gl.Vertex(1.0f, -1.0f, 1.0f);
gl.Color(1.0f, 0.0f, 0.0f);
gl.Vertex(0.0f, 1.0f, 0.0f);
gl.Color(0.0f, 0.0f, 1.0f);
gl.Vertex(1.0f, -1.0f, 1.0f);
gl.Color(0.0f, 1.0f, 0.0f);
gl.Vertex(1.0f, -1.0f, -1.0f);
gl.Color(1.0f, 0.0f, 0.0f);
gl.Vertex(0.0f, 1.0f, 0.0f);
gl.Color(0.0f, 1.0f, 0.0f);
gl.Vertex(1.0f, -1.0f, -1.0f);
gl.Color(0.0f, 0.0f, 1.0f);
gl.Vertex(-1.0f, -1.0f, -1.0f);
gl.Color(1.0f, 0.0f, 0.0f);
gl.Vertex(0.0f, 1.0f, 0.0f);
gl.Color(0.0f, 0.0f, 1.0f);
gl.Vertex(-1.0f, -1.0f, -1.0f);
gl.Color(0.0f, 1.0f, 0.0f);
gl.Vertex(-1.0f, -1.0f, 1.0f);
gl.End();
// Reset the modelview.
gl.LoadIdentity();
// Move into a more central position.
gl.Translate(1.5f, 0.0f, -7.0f);
// Rotate the cube.
gl.Rotate(rquad, 1.0f, 1.0f, 1.0f);
// Provide the cube colors and geometry.
gl.Begin(OpenGL.GL_QUADS);
gl.Color(0.0f, 1.0f, 0.0f);
gl.Vertex(1.0f, 1.0f, -1.0f);
gl.Vertex(-1.0f, 1.0f, -1.0f);
gl.Vertex(-1.0f, 1.0f, 1.0f);
gl.Vertex(1.0f, 1.0f, 1.0f);
gl.Color(1.0f, 0.5f, 0.0f);
gl.Vertex(1.0f, -1.0f, 1.0f);
gl.Vertex(-1.0f, -1.0f, 1.0f);
gl.Vertex(-1.0f, -1.0f, -1.0f);
gl.Vertex(1.0f, -1.0f, -1.0f);
gl.Color(1.0f, 0.0f, 0.0f);
gl.Vertex(1.0f, 1.0f, 1.0f);
gl.Vertex(-1.0f, 1.0f, 1.0f);
gl.Vertex(-1.0f, -1.0f, 1.0f);
gl.Vertex(1.0f, -1.0f, 1.0f);
gl.Color(1.0f, 1.0f, 0.0f);
gl.Vertex(1.0f, -1.0f, -1.0f);
gl.Vertex(-1.0f, -1.0f, -1.0f);
gl.Vertex(-1.0f, 1.0f, -1.0f);
gl.Vertex(1.0f, 1.0f, -1.0f);
gl.Color(0.0f, 0.0f, 1.0f);
gl.Vertex(-1.0f, 1.0f, 1.0f);
gl.Vertex(-1.0f, 1.0f, -1.0f);
gl.Vertex(-1.0f, -1.0f, -1.0f);
gl.Vertex(-1.0f, -1.0f, 1.0f);
gl.Color(1.0f, 0.0f, 1.0f);
gl.Vertex(1.0f, 1.0f, -1.0f);
gl.Vertex(1.0f, 1.0f, 1.0f);
gl.Vertex(1.0f, -1.0f, 1.0f);
gl.Vertex(1.0f, -1.0f, -1.0f);
gl.End();
// Flush OpenGL.
gl.Flush();
// Rotate the geometry a bit.
rotatePyramid += 3.0f;
rquad -= 3.0f;
}
float rotatePyramid = 0;
float rquad = 0;
按 F5 - 看看我们得到了什么。

好吧,我们得到了旋转的金字塔和立方体,但很明显我们遇到了一些问题 - 每个模型的面都按照它们定义的顺序绘制,并且互相覆盖 - 深度缓冲区不起作用!
与用于 OpenGL
绘制的事件一样,也有一个用于进行 OpenGL
初始化的事件。下面是我们首先进行一些 OpenGL
初始化的方法。处理 OpenGL
控件的 OpenGLInitialized
事件。
<sharpGL:OpenGLControl OpenGLDraw="OpenGLControl_OpenGLDraw"
OpenGLInitialized="OpenGLControl_OpenGLInitialized" />
我们在代码隐藏中得到以下函数 - 现在只需启用深度测试功能。
private void OpenGLControl_OpenGLInitialized(object sender, OpenGLEventArgs args)
{
// Enable the OpenGL depth testing functionality.
args.OpenGL.Enable(OpenGL.GL_DEPTH_TEST);
}
太棒了!作为一种性能检查,我们可以将 'DrawFPS="True"
' 添加到我们的 OpenGLControl
XAML 中,以查看帧速率。

请注意,绘制时间是正确的,FPS 是可以基于此绘制时间使用的值,而不是实际使用的值。默认 FPS 为 28,但 OpenGLControl
有一个 FrameRate
属性,您可以将其设置为任意值。
投影
OpenGLControl
默认会为投影矩阵创建一个基本的透视变换,但在任何实际应用程序中,您都希望自己进行处理。在控件的 Resized
事件中执行透视变换,如下所示。
<sharpGL:OpenGLControl
OpenGLDraw="OpenGLControl_OpenGLDraw"
OpenGLInitialized="OpenGLControl_OpenGLInitialized"
DrawFPS="True"
Resized="OpenGLControl_Resized" />
…以及代码隐藏…
private void OpenGLControl_Resized(object sender, OpenGLEventArgs args)
{
// Get the OpenGL instance.
OpenGL gl = args.OpenGL;
// Load and clear the projection matrix.
gl.MatrixMode(OpenGL.GL_PROJECTION);
gl.LoadIdentity();
// Perform a perspective transformation
gl.Perspective(45.0f, (float)gl.RenderContextProvider.Width /
(float)gl.RenderContextProvider.Height,
0.1f, 100.0f);
// Load the modelview.
gl.MatrixMode(OpenGL.GL_MODELVIEW);
}
RenderContextProvider
是一个内部对象,用于抽象 OpenGL
Render
上下文的管理方式。它提供了渲染图面的像素宽度和高度。将在第二部分中详细介绍。
结论
此示例演示了如何使用一些简单的 OpenGL
函数执行一些简单的渲染。SharpGL 2.0 实际上包含了所有主要的 OpenGL
扩展以及所有核心功能,直到 OpenGL
4.2,因此您可以使用它来制作一些非常酷的东西。
第二部分:它是如何工作的?
通常,当执行 OpenGL
绘制时,它是针对本机 Win32 窗口句柄的设备上下文进行渲染的。事实上,这对于进行任何 OpenGL
绘制基本上是必需的。
还有另一种方法 - 可以创建一个将绘制到 DIB(设备无关位图)的设备上下文,这样就不需要窗口了。然后,我们可以直接将 DIB 位图绘制到 WPF 控件。但是,这有一个严重的限制 - 绘制到 DIB 永远不会进行硬件加速,它始终使用 Windows 附带的原生 OpenGL
1.1 驱动程序。它不仅不是硬件加速的,而且不支持任何现代扩展。
那么,如何在没有窗口的情况下绘制到内存(以便我们可以绘制到 WPF)?最好的方法是使用 OpenGL
Framebuffer
对象。OpenGL Framebuffer
扩展允许绘制到内存而不是窗口。它提供了许多高级功能,例如将场景的深度分量直接渲染到纹理,但也能让我们在没有窗口的情况下进行渲染。
真的没有窗口?不。要创建具有扩展(包括我们所需的帧缓冲区扩展)访问权限的 OpenGL
实例,我们仍然需要从与双缓冲窗口关联的设备上下文创建 OpenGL
渲染上下文。因此,SharpGL
在内部创建一个隐藏窗口,从中创建一个渲染上下文,然后将绘制重定向到帧缓冲区。每帧绘制完成后,帧缓冲区的内容都会绘制到 OpenGLControl
- 这意味着我们有一个可以进行硬件加速、支持扩展并且不会像直接插入 WinFormsHost
那样遭受“空气间隙”问题的 OpenGL
控件!
渲染上下文提供程序
这是相当复杂的逻辑(创建帧缓冲区等),其目的是创建 OpenGL
渲染上下文。SharpGL
实际上支持渲染到 DIB、本机窗口,甚至隐藏窗口(在 Windows XP 中可以将其 blit 到屏幕)。由于每种渲染方式都不同,我们引入了 RenderContextProvider
的概念 - 一个处理创建、调整大小和清理 OpenGL
渲染上下文及其支持对象的内部逻辑的对象。这就是为什么在前面的示例中,我们使用 OpenGL
对象的 RenderContextProvider
属性来获取像素宽度和高度。
延伸阅读
如果您有兴趣了解有关 SharpGL
的更多信息,这里有一些有用的链接。
- CodePlex 上的
SharpGL
: http://sharpgl.codeplex.com/ - 我的博客: http://www.dwmkerr.com/
- 原始
SharpGL
CodeProject - 文章: https://codeproject.org.cn/KB/openGL/sharpgl.aspx
历史
- 2011 年 10 月 9 日:初始帖子