65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 应用程序中使用 OpenGL

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (23投票s)

2011 年 10 月 9 日

CPOL

6分钟阅读

viewsIcon

228908

downloadIcon

13327

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

MainWindow-Final.png

引言

在本文中,我将向您展示如何直接在 WPF 控件中渲染 OpenGL,而无需任何窗口句柄或 WinFormsHost 对象等“技巧”。

我们将做的第一件事是创建一个执行某些 OpenGL 渲染的项目。在此之后,我将描述其内部工作原理 - 因此,如果您只想快速上手 OpenGL,只需阅读文章的第一部分即可。

Beta 说明:本文使用的是 SharpGL 2.0 Beta 1 版本 - 这是一个 beta 版本,因此在完整版本可用时可能会稍有变动。

第一部分:WPF 中的 OpenGL 渲染

这将非常简单 - 第一件事是获取最新版本的 SharpGLSharpGLOpenGL 库的 CLR 包装器 - 它支持硬件加速,并拥有从 OpenGL 1.1 到最新版本的 OpenGL 4.2 的所有核心功能和扩展。

从 CodePlex 下载页面获取核心二进制文件。这是下载页面,您需要核心二进制文件

或者,如果您愿意,也可以从文章顶部的链接下载核心二进制文件。

入门

创建一个新的 WPF 应用程序,命名为 WPFOpenGL。现在,将下载的 SharpGLSharpGL.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 客户端配置文件。

TargetFramework.png

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

MainWindow-Empty.png

这是因为我们还没有进行任何渲染。现在让我们继续。

使用 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 - 看看我们得到了什么。

MainWindow-DepthProblems.png

好吧,我们得到了旋转的金字塔和立方体,但很明显我们遇到了一些问题 - 每个模型的面都按照它们定义的顺序绘制,并且互相覆盖 - 深度缓冲区不起作用!

与用于 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 中,以查看帧速率。

MainWindow-Final.png

请注意,绘制时间是正确的,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 的更多信息,这里有一些有用的链接。

历史

  • 2011 年 10 月 9 日:初始帖子
© . All rights reserved.