Click here to Skip to main content
13,861,604 members
Click here to Skip to main content
Add your own
alternative version

Stats

20.7K views
751 downloads
13 bookmarked
Posted 21 Oct 2015
Licenced CPOL

Getting startet with OpenGL/OpenTK in MONO/.NET for serious applications

, 28 Nov 2018
Rate this:
Please Sign up or sign in to vote.
Check OpenGL as a basis for appealing applications, that are not necessarily games.

Download OIpenGL-Test1.zip Oct. 2015 (MonoDevelop C# solution, including source and debug binary)

Download OIpenGL-Test2.zip Nov. 2018 (MonoDevelop C# solution, including source and debug binary)

Introduction

My basic interest is to evaluate the opportunities to program an appealing serious/business/GUI-centered application for Unix/Linux using Mono, that is based on an up-to-date 2D/3D rendering engine (OpenGL) - remotely comparable to a WPF application for Windows using .NET, that is based on DirectX.

Background

There are a lot of good internet articles how to start programming with OpenGL for any possible OS and programming language. But after many hours of reading indroductions, tutorials and API documentation, i still do not feel sure about

  • the best toolkit (up-to-date, actively maintained, feature-rich, well documented) for C# OpenGL programming
    • OpenTK  - the most thin layer around OpenGL and most detailed documentation,
    • Pencil.Gaming (read this as well) - thin layer around OpenGL, GLEW and other stuff,
    • SFML.Net - a C# wrapper for SFML feature-rich platform abstraction for OpenGL and other stuff,
    • SDL2# - a C# wrapper for SDL very feature-rich platform abstraction for OpenGL and other stuff,
    • exotic/underground alternatives and
  • the best programming aproach (might depend on the toolkit) to implement a "no-game" application.

Annotation: Don't hesitate to comment my list of toolkits - i might have overlooked a cool one.

Nevertheless i want to share the knowledge i gained so far. I think i'll update this article in the future to share new findings. According to the list of toolkits above, i decided to use OpenTK, because

  • i have to learn the basics of OpenGL before i can judge the quality of toolkits that wrap/abstract OpenGL and OpenTK is the the most thin layer around OpenGL and gives native OpenGL feeling
  • the OpenTK home page is the most detailed one and seems to be the most active one

even if i already know before i start that

  • OpenTK is known to have trouble with windowing (the OpenGL rendering engine must always reside inside/be attached to an application window or control), especially on OS X, and
  • the programming effort might be the highest.

I recommend to read Bartlomiej Filipek's article Learning Modern OpenGL at code project to clear the very basic questions.

Using the code

Prerequisits

To (start at the very beginning and to) be sure about the prerequisits

  • i installed a fresh openSuse Linux 13.2 Tumbleweed x86_64 DE
  • within a vmWare Player 7.1.2 build-2780323 running on Windows 8.1
  • selecting only the basic X11 installation with XFCE desktop (no GNOME and no KDE to prevent side effects).

Since the XFCE desktop is GTK based, the GTK2 libraries are installed as well.

Without any explicit selection, a basic Mesa installation has already been performed by the initial setup.

Subsequently i installed MonoDevelop. This package includs the dependecies to the Mono runtime as well.

There seem to be two package dependency errors and MonoDevelop didn't start until MonoDoc-Core and mono-locale-extras are installed.

The last prerequisit is to download the OpenTK and provide it for the project. My choice was to use the /opentk-1.1/stable-5/opentk-2014-07-23.zip, that contains the required assemblies within /Binaries/OpenTK/Release. The *.dll.config files already contain the library references for Linux and OS X. The /README.md is helpful.

Now a new .NET/Empty project solution can be created targeting Any CPU (i named my project OglAppealingApplTest) and a /References folder can be prepared with the required assemblies.

Project

My OpenGL-Test1 solution / OglAppealingApplTest project has been created with MonoDevelop 5.0.1 on Mono 3.8.0 and looks like:

References (Verweise):
- OpenTK.dll contains all the needed stuff,
   OpenTK.GLControl.dll shall be avoided for my objectives
   OpenTK.Compatibility.dll is not required (no Tao)
- Since OpenTK utilizes Mono's System.Forms implementation,
  the System.Drawing reference is required.

OpenTK folder
- contains extensions directly related to OpenGL
   (colors, brushes, converters, ...)

System folder
- contains the GUI controls, used for the test application
   The GUI controls are oriented to the WPF controls, that belong
   completely to the System.Windows namespace.

Update with article version 2.0

I've updated MonoDevelop 5.0.1 to MonoDevelop 5.10 to overcome the frequent debugger crashes. And i changed the target platform from x86 to AnyCPU.

Go on with the original article

The compiled application shows a triangle filled with a linear gradient in the background and three absolute positioned buttons. Button 3 closes the window.

The Buttons are children of a Canvas. The API  of all controls is oriented to the WPF controls.

The control creation looks like this:

public MyWindow ()
    : base ()
{
    this.Resize += HandleResize;

    Canvas layoutManager = new Canvas();
    this.Content = layoutManager;

    Button button1 = new Button();
    button1.X = 50;
    button1.Y = 50;
    button1.Width = 100;
    button1.Height = 50;
    layoutManager.AddChild(button1);
    button1.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 1 clicked.");
    };
    TextBlock text1 = new TextBlock ();
    text1.Text = "Button 1";
    text1.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Red);
    button1.Content = text1;
    
    Button button2 = new Button();
    button2.X = 50;
    button2.Y = 150;
    button2.Width = 100;
    button2.Height = 50;
    layoutManager.AddChild(button2);
    button2.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 2 clicked.");
    };
    TextBlock text2 = new TextBlock ();
    text2.Text = "Button 2";
    text2.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Green);
    button2.Content = text2;

    Button button3 = new Button();
    button3.X = 50;
    button3.Y = 250;
    button3.Width = 100;
    button3.Height = 50;
    layoutManager.AddChild(button3);
    button3.Click += delegate(object sender, EventArgs e)
    {
        SimpleLog.LogLine (TraceEventType.Information, "Button 3 clicked.");
        this.Close ();
    };
    TextBlock text3 = new TextBlock ();
    text3.Text = "Button 3";
    text3.Foreground = new OpenTK.Media.SolidColorBrush (Color4.Blue);
    button3.Content = text3;
}

The application runs with the Window.Show() method call, that handles the events.

Since there is no Invalidate() (Windows.Forms), ExposeEvent (X11) or WM_PAINT message (Win32), a _invalidated flag does just this job.

Update with article version 2.0

First I've outsourced some of the original Window.Show() method code to base.ProcessMessage(). This separates the (infinite) loop while (_glWindow.Exists) from the real message processing and enables me to call base.ProcessMessage() method fom several sources.

/// <summary>Shows this application window instance.</summary>
public virtual void Show ()
{
    UpdateLayout ();

    _glWindow.Visible = true;

    while (_glWindow.Exists)
    {
        bool hasRedrawn = base.ProcessMessage();
        if(!hasRedrawn)
            Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION);
    }
    GuiThreadDispatcher currentDispatcher = GuiThreadDispatcher.CurrentDispatcher;
    currentDispatcher.Dispatch ();

    _glWindow.Visible = false;
}

Second I've added _context.MakeCurrent(_glWindow.WindowInfo) twice. This enables me to call base.ProcessMessage() method fom different GL windows utilizing different GL contexts.

/// <summary>Processes one GL window message.</summary>
/// <returns>Returns <c>true</c>, if display has been validated, or <c>false</c> otherwise.</returns>
/// <remarks>While a typical X11 message loop (utilizing XNextEvent()) blocks until
/// the next event arrives (and saves CPU power), a typical GL message loop
/// (utilizing ProcessEvents()) doesn't block (and wastes CPU power).</remarks>
public bool ProcessMessage()
{
    if (!_glWindow.Exists)
        return false;

    // Can be called from child windows (with own GL contect) as well.
    // Thus we have to ensure the right GL context.
    if (!_context.IsCurrent)
    {
        SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
                  "::ProcessMessage() Reactivating GL context={0} ...", _context.GetHashCode());
        _context.MakeCurrent(_glWindow.WindowInfo);
    }

    _glWindow.ProcessEvents ();
    // Calls implementation.ProcessEvents ():
    //       Just delegates complete processing to implementation.
        // Calls LinuxNativeWindow.ProcessEvents ():
        //       Just calls ProcessKeyboard() and ProcessMouse().
            // Calls NativeWindowBase.ProcessEvents ():
            //       Just clears keyboard, to prevent confusion on missing KeyUp.
    if (_invalidated)
    {
        // During event processing, new GL windows (with own GL contect) can be created.
        // Thus we have to ensure the right GL context.
        if (!_context.IsCurrent)
        {
            SimpleLog.LogLine (TraceEventType.Information, CLASS_NAME +
                      "::ProcessMessage() Reactivating GL context={0} ...", _context.GetHashCode();
            _context.MakeCurrent(_glWindow.WindowInfo);
        }

        // ===================================================
        // Prepare the OpenGL environment and draw background.
        // ===================================================
        GL.ClearColor(Color.LightSalmon);
        GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

        GL.MatrixMode(MatrixMode.Projection);
        GL.LoadIdentity();
        GL.MatrixMode(MatrixMode.Modelview);
        GL.LoadIdentity();

        GL.Ortho(0, _glWindow.Width, _glWindow.Height, 0, -1, 1);
        GL.Viewport(0, 0, _glWindow.Width, _glWindow.Height);

        // ===================================================
        // OpenGL must render buffers at once.
        // ===================================================
        UpdateRender();

        // ===================================================
        // Finalize drawing.
        // ===================================================
        GL.Flush();
        _context.SwapBuffers();

        _invalidated = false;

        return true;
    }
    else
        return false;
}

Both changes are prerequisites to open sub windows - e.g. dialog boxes. I've added a dialog box sample utilizing a MessageBox. The code is an excerpt from the control creation:

...
button1.Click += delegate(object sender, EventArgs e)
{
    SimpleLog.LogLine (TraceEventType.Information, "Button 1 clicked.");

    Dialog subWindow = new MessageBox (this, 800, 150, "OpenTK Message Box",
                                       "Hello!\nThis is a dialog box.");
    subWindow.ShowModal();
};
...

The result looks like:

Go on with the original article

Strange behaviours

Painting

Display update

WPF, I've chosen as a prototype (becaus it is based on a graphics hardware accelleration DirectX just like my project is based in OpenGL), has no Invalidate() (Windows.Forms), ExposeEvent (X11) or WM_PAINT message (Win32) as well. Instead the only way to invoke a paint is to call InvalidateVisual() (forces a complete new layout pass which calls UpdateLayout() and calls UpdateRender() subsequently to redraw). See Invalidate own WPF control and Data binding performance issues for a discussion about drawbacks.

Up to the finding of a good solution the Window class of my implementation provides the Invalidate() method to set the private _invalidated flag.

Currently all OpenGL render instructions must draw at once and end up with SwapBuffers().

Update with article version 2.0

Multiple windows

I've implemented a very lightweight approach to support multiple windows: Since the creation of a sub window interupts the main window message loop, a sub window message loop cares for the main window messages a well. This approach was easy to realize after the separation of the (infinite) loop while (_glWindow.Exists) from the real message processing. A sub window message loop looks like:

/// <summary>Shows this dialog window instance.</summary>
/// <remarks>This will interrupt the <see cref="ParentWindow"/>'s message loop and
/// handle it's messages here. </remarks>
public virtual void ShowModal ()
{
    _isModal = true;
    UpdateLayout ();

    _glWindow.Visible = true;

    while (_glWindow.Exists)
    {
        bool hasRedrawn = base.ProcessMessage();
        if (_parentWindow != null)
            hasRedrawn |= _parentWindow.ProcessMessage();

        if(!hasRedrawn)
            Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION);
    }

    GuiThreadDispatcher currentDispatcher = GuiThreadDispatcher.CurrentDispatcher;
    currentDispatcher.Dispatch ();

    _glWindow.Visible = false;
}

Currently this solution realizes these features whilea  modal dialog window is shown:

window feature expectation result
main redraw after invalidation redrawing, e.g. after resize OK
main redraw after interaction redrawing, e.g. hover effect OK
main ignore user interaction ignore click event OK
main ignode window manager commands ignore close from window frame missing
sub process user interaction process click event OK

CPU usage

While a typical X11 message loop (utilizing XNextEvent()) blocks until the next event arrives (and saves CPU power), a typical GL message loop (utilizing ProcessEvents()) doesn't block (and wastes CPU power). To minimize this negative effect, i call Thread.Sleep(SLEEP_ON_NO_DISPLAY_VALIDATION) after every event loop, that doesn't validate the display.

Go on with the original article

Text rendering

The easiest approach to render text seems to be the application of System.Drawing.Graphics.DrawString(). Mono has implemented this method as a base for it's System.Windows.Forms implementation on X11. This characterizes the major advantage - such a text rendering is Windows/X11 platform independend.

The technical iplementation draws the text on a bitmap, marks the bitmap background color to be the transparent color of the bitmap and renders the bitmap as a texture with blending into the scene. To prevent off-colors, the scene background color defines the bitmap background color. This approach produces aceptable or good results, depending on the font size, font face and background/foreground contrast.

Unfornately Mono's System.Drawing.Graphics.DrawString() implementation don't care for System.Drawing.Graphics.TextRenderingHint, it's text rendering always looks like System.Drawing.Text.TextRenderingHint.AntiAlias. This causes two problems:

A better approach should be the application of FreeType, but this is to be proven.

Update with article version 2.0

To overcome the text rendering drawbacks a reference to the OpenFW library has been added. OpenFW has been developed for OpenGL text rendering with FreeType within this article: Abstract of the text rendering with OpenGL/OpenTK in MONO/.NET Please check this article for source code and updates.

Prerequisits

The next image shows the newly referenced assembly file.

.

Project

The project has grown slightly compared to version 1.

References (Verweise) (updated):
- OpenFW OpenTK.dll contains all the needed stuff,
   OpenTK.GLControl.dll shall be avoided for my objectives
   OpenTK.Compatibility.dll is not required (no Tao)

Images folder (new):
- contains a lot of roalty-free images for various purposes and most of
  them in multiple sizes from, the MessageBoxImages among them.
  The images are taken from the Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Introduction article.
  Please check this article for source code and updates.

OpenTK folder (unchanged)

Properties folder (new):
- contains generic image and text properties

System folder (updated)

The next two images compare the initial text rendering utilizing System.Drawing.Graphics.DrawString() to the new one utilizing OpenFW.

This is the code (old one and new one)...

if (legacyTextRenering)
{
    if (glTxColor == Color4.Transparent)
    {
        List<Visual> parentHierarchy = VisualTreeHelper.ParentVisualHierarchy (this);
        for (int index = 0; index < parentHierarchy.Count; index++)
        {
            if (parentHierarchy [index] as Control != null)
            {
                bgBrush = (parentHierarchy [index] as Control).Background;
                if (bgBrush as OpenTK.Media.SolidColorBrush != null)
                {
                    Color4 bgColor = (bgBrush as OpenTK.Media.SolidColorBrush).Color;
                    glTxColor = bgColor;
                    if (glTxColor != Color4.Transparent)
                        break;
                }
            }
        }
    }

    // Choose a neutrally texture background color as a falback for transparent color.
    if (glTxColor == Color4.Transparent)
        glTxColor = Color4.Gray;
    // Calculate the texture background.
    System.Drawing.Color msTxColor = System.Drawing.Color.FromArgb (glTxColor.ToArgb());
    
    // Calculate the texture foreground (text) color.
    OpenTK.Media.Brush glFgBrush = Foreground;
    System.Drawing.Brush msFgBrush = new System.Drawing.SolidBrush (System.Drawing.Color.Black);
    if (glFgBrush as OpenTK.Media.SolidColorBrush != null)
    {
        System.Drawing.Color glFgColor = System.Drawing.Color.FromArgb (
            (glFgBrush as OpenTK.Media.SolidColorBrush).Color.ToArgb ());
        msFgBrush = new System.Drawing.SolidBrush (glFgColor);
    }

    // Prepare texture map.
    TextRenderer textRenderer = new TextRenderer(
        (int)(Width - margin.Left - margin.Right + 0.49),
        (int)(Height - margin.Top - margin.Bottom + 0.49));
    textRenderer.Clear(msTxColor);

    // Draw text to the texture map.
    PointF position = PointF.Empty;
    textRenderer.DrawString(text, ThemeManager.CurrentTheme.DefaultFont, msFgBrush, position);

    GL.Enable(EnableCap.Texture2D);
    GL.Enable(EnableCap.Blend);
    GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);

    GL.BindTexture(TextureTarget.Texture2D, textRenderer.Texture(msTxColor));
    GL.Begin(PrimitiveType.Quads);
    GL.Color3 (glTxColor.R, glTxColor.G, glTxColor.B); // System.Drawing.Color.Yellow); // msColor

    //Address: Left-Top       Set coordinates: X, Y
    GL.TexCoord2(0.0f, 0.0f); GL.Vertex2((float)(X + margin.Left), (float)(Y + margin.Top));
    //Address: Right-Top      Set coordinates: X, Y
    GL.TexCoord2(1.0f, 0.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
                                         (float)(Y + margin.Top));
    //Address: Right-Bottom   Set coordinates: X, Y
    GL.TexCoord2(1.0f, 1.0f); GL.Vertex2((float)(X + margin.Left) + textRenderer.Width,
                                         (float)(Y + margin.Top) + textRenderer.Height);
    //Address: Left-Bottom    Set coordinates: X, Y
    GL.TexCoord2(0.0f, 1.0f); GL.Vertex2((float)(X + margin.Left),
                                         (float)(Y + margin.Top) + textRenderer.Height);

    GL.End();
    GL.BindTexture(TextureTarget.Texture2D, 0);

    textRenderer.Dispose();
}
else
{
    System.Drawing.Color ftFgColor =
        System.Drawing.Color.FromArgb ((Foreground as OpenTK.Media.SolidColorBrush).Color.ToArgb ());
    FtText ftText = new FtText (Font, text,
        FtFontFace.LineSizeToCharacterSize(ThemeManager.CurrentTheme.DefaultFont.Height), false,
        GlUtil.GlyphVisualEffects.None, (int)(X + margin.Left + 0.49f), (int)(Y + margin.Top + 0.49f),
        ftFgColor, false, true);
    ftText.Draw ();
}
Go on with the original article

Points of Interest

Are appealing serious/business/GUI-centered application for Unix/Linux based on OpenGL realistic? - Yes.

Is it possible to provide a WPF-like API? - Yes.

Are there pitfalls? - Yes, render instructions must draw at once and text rendering is worthy of improvement.

History

Initial version from 21. October 2015.
Second version from 27. November 2018.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Steffen Ploetz
CEO Ploetz + Zeller GmbH
Germany Germany
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
SuggestionHave a look at that project : you may have some common goals Pin
mikipatate7-Nov-16 1:17
membermikipatate7-Nov-16 1:17 
GeneralRe: Have a look at that project : you may have some common goals Pin
Steffen Ploetz7-Nov-16 3:45
professionalSteffen Ploetz7-Nov-16 3:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web06 | 2.8.190214.1 | Last Updated 29 Nov 2018
Article Copyright 2015 by Steffen Ploetz
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid