OpenTK, entering the C# 3D world

I was amazed how short the source code can be to show complex moving objects. It does take a bit of research and the OpenTK library though. There are a lot of very good YouTube tutorials out there. Not necessarily about OpenTK, but plenty about OpenGL, which is what OpenTK is using.

I asked my daughters to draw some Minecraft style avatars with a simple icon editor. They had their fun and I got something to display in 3D. I stored three icon files (Freya.ico, Merlin.ico and Steve.ico) in a subfolder called ‘Resources’.

OpenTk

Change the file properties. ‘Build Action’ should be set to ‘None’, because you don’t have to compile these files. And you don’t want to copy the files each time you run the compiler. Simply set ‘Copy to Output Directory’ to ‘Copy if newer’.

OpenTk2

The next step is to install and reference the OpenTK library (OpenTK, OpenTK.Compatibility and OpenTK.GLControl). Be aware that there is no real WPF support for OpenTK. You only host a Winform control within WPF. Therefore you should also reference “WindowsFormsIntegration”.
You can also open a pure OpenTK window. But it is impossible to add further WPF controls to the same window then. We are far away from programming games, so it is always a good option to leave the backdoor open for further WPF. You might want to add sliders to set angles and object positions.

 

OpenTk3

 

Sorry for posting less for a while. I am working like a dog – leaving home at 6am and returning around 10pm during the week.

 

<Window x:Class="OpenTkControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WinF="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        xmlns:OpenTK="clr-namespace:OpenTK;assembly=OpenTK.GLControl"
        Title="OpenTK Demo" Height="600" Width="800">
  <DockPanel LastChildFill="True">
    <WindowsFormsHost  x:Name="WinFormsContainer" Background="Transparent" DockPanel.Dock="Top"  >
      <OpenTK:GLControl x:Name="OpenTkControl" 
                        Paint="OpenTkControl_Paint" Dock="Fill" />
    </WindowsFormsHost>
  </DockPanel>
</Window>
using OpenTK;
using OpenTK.Graphics.OpenGL;
using System;
using System.Drawing;
using System.Windows;
using System.Windows.Threading;

namespace OpenTkControl {
  public partial class MainWindow : Window {

    private DispatcherTimer _Timer;
    private DateTime _ProgramStartTime;

    public MainWindow() {
      InitializeComponent();

      _ProgramStartTime = DateTime.Now;

      _Timer = new DispatcherTimer(DispatcherPriority.Send);
      _Timer.IsEnabled = true;
      _Timer.Interval = new TimeSpan(0, 0, 0, 0, 30);
      _Timer.Tick += OnTimer;
      _Timer.Start();
    } // constructor

    void OnTimer(object sender, EventArgs e) {
      OpenTkControl.Invalidate();
    } //

    private void OpenTkControl_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
      GLControl lControl = OpenTkControl;

      // Reset the depth and color buffer.
      // We want to render a new world. We do not want to continue with a previous rendering.
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

      // Create a projection matrix transforming camera space to raster space. (google for "view frustum")
      // Which is like: Press the 3D world and make it flat like a pancake, so that it does fit on the 2D screen.
      //                All points between a distance of 1 and 1000 will be taken into account.
      float lAngleView = 1.2f;              // y direction (in radians)
      float lAspectRatio = 4f / 3f;         // width / height
      float lDistanceToNearClipPlane = 1f;
      float lDistanceToFarClipPlane = 1000f;
      Matrix4 lPerspective = Matrix4.CreatePerspectiveFieldOfView(lAngleView, lAspectRatio, lDistanceToNearClipPlane, lDistanceToFarClipPlane);
      GL.MatrixMode(MatrixMode.Projection); GL.LoadIdentity(); GL.LoadMatrix(ref lPerspective);

      // camera setup
      Vector3 lCameraLocation = new Vector3(100f, 10f, 0f);
      Vector3 lCameraLookingAt = new Vector3(0f, 0f, 0f);     // look at the center of the coordinate system
      Vector3 lCameraWhatIsUpside = new Vector3(0f, 1f, 0f);  // classical way to hold a camera
      Matrix4 lCamera = Matrix4.LookAt(lCameraLocation, lCameraLookingAt, lCameraWhatIsUpside);
      GL.MatrixMode(MatrixMode.Modelview); GL.LoadIdentity(); GL.LoadMatrix(ref lCamera);

      // this is the size on the screen
      GL.Viewport(0, 0, lControl.Width, lControl.Height);

      // only draw the nearest pixels and not pixels that are actually hidden by other pixels 
      GL.Enable(EnableCap.DepthTest);
      GL.DepthFunc(DepthFunction.Less);

      // set time dependent variables to generate movements
      double lTotalMillis = DateTime.Now.Subtract(_ProgramStartTime).TotalMilliseconds;
      double lTime1 = (lTotalMillis % 10000.0) / 10000.0; // between 0 and 1
      double lTime2 = (lTotalMillis % 2000.0) / 2000.0;   // between 0 and 1
      double lTimeRadians = lTime2 * 2.0 * Math.PI;
      float lJump = (float)(-20.0 + 10.0 * Math.Sin(lTimeRadians));
      float lRadius = -40f;

      // add the comet
      DrawComet(lTotalMillis);

      // render the floor
      GL.Rotate(360.0 * lTime1, 0.0, 1.0, 0.5);  // rotate around y axis and half as much around z axis
      DrawFloor();

      // render objects
      // from where we are; now rotate the objects into the opposite direction
      GL.Rotate(-lTime1 * 360.0 * 2.0, 0.0, 1.0, 0.0); DrawAvatar("Merlin", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Freya", lJump, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Steve", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Merlin", lJump, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Freya", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Steve", lJump, lRadius);

      // render the cube in the center
      //GL.Rotate(360f * lTime2, 0f, 0f, 0f); // <= this kind of rotation lets the box bounce and change its size
      DrawCube(Color.SteelBlue, Color.DarkBlue, 0f, -25f, 0f, 8f, false);

      OpenTK.Graphics.GraphicsContext.CurrentContext.VSync = true; // caps GPU frame rate
      lControl.SwapBuffers();  // display our newly generated buffer with all objects
    } //

    private void DrawAvatar(string xName, float yShift, float zShift) {
      Icon lIcon = new Icon("Resources/" + xName + ".ico");
      Bitmap lBitmap = lIcon.ToBitmap();
      int lWidth = lBitmap.Width; float lHalfWidth = lWidth / 2f;
      int lHeight = lBitmap.Height; float lHalfHeight = lHeight;
      for (int y = 0; y < lHeight; y++) {
        for (int x = 0; x < lWidth; x++) {
          Color lColor = lBitmap.GetPixel(x, y);
          if (lColor.A != 0) DrawCube(lColor, lColor, (float)x - lHalfWidth, lHeight + yShift - (float)y, (float)zShift, 1f, true);
        }
      }
    } //

    private void DrawFloor() {
      for (int x = -100; x < 100; x += 10) {
        for (int z = -100 + (x % 10 == 0 ? 5 : 0); z < 100; z += 10) {
          DrawCube(Color.White, Color.Gray, x, -30f, z, 5f, false);
        }
      }
    } //

    private void DrawComet(double xTotalMillis) {
      xTotalMillis = (xTotalMillis % 7000.0) / 7000.0; // between 0 and 1

      GL.PushMatrix();
      GL.LoadIdentity();
      GL.Translate(xTotalMillis * 30f - 40f , 40f,  400f * xTotalMillis - 400f);
      GL.Rotate(360f * xTotalMillis * 3f, 1f, 1f, 1f);
      DrawTetrahedron(Color.Orange, Color.OrangeRed, 0f, 0f, 0f, 8f);
      GL.Rotate(180f, 1f, 0f, 0f);
      DrawTetrahedron(Color.Orange, Color.OrangeRed, 0f, 0f, 0f, 8f);
      GL.PopMatrix();
    } //

    private void DrawCube(System.Drawing.Color xColor, System.Drawing.Color xColor2, float X, float Y, float Z, float xWidth, bool xHasDarkBack) {
      float lHalfWidth = xWidth / 2f;
      float lTop = Y + lHalfWidth;
      float lBottom = Y - lHalfWidth;
      float lLeft = X - lHalfWidth;
      float lRight = X + lHalfWidth;
      float lFront = Z + lHalfWidth;
      float lRear = Z - lHalfWidth;

      GL.Begin(PrimitiveType.Quads);

      Color lColor; if (xHasDarkBack) lColor = Color.DarkGray; else lColor = xColor;
      Color lColor2; if (xHasDarkBack) lColor2 = Color.DarkGray; else lColor2 = xColor2;

      Action lPointFrontTopLeft = () => { GL.Color3(xColor); GL.Vertex3(lLeft, lTop, lFront); };
      Action lPointFrontTopRight = () => { GL.Color3(xColor2); GL.Vertex3(lRight, lTop, lFront); };
      Action lPointFrontBottomLeft = () => { GL.Color3(xColor2); GL.Vertex3(lLeft, lBottom, lFront); };
      Action lPointFrontBottomRight = () => { GL.Color3(xColor2); GL.Vertex3(lRight, lBottom, lFront); };
      Action lPointRearTopLeft = () => { GL.Color3(lColor); GL.Vertex3(lLeft, lTop, lRear); };
      Action lPointRearTopRight = () => { GL.Color3(lColor2); GL.Vertex3(lRight, lTop, lRear); };
      Action lPointRearBottomLeft = () => { GL.Color3(lColor2); GL.Vertex3(lLeft, lBottom, lRear); };
      Action lPointRearBottomRight = () => { GL.Color3(lColor2); GL.Vertex3(lRight, lBottom, lRear); };

      // front square
      lPointFrontTopLeft(); lPointFrontTopRight(); lPointFrontBottomRight(); lPointFrontBottomLeft();

      // rear square
      lPointRearTopLeft(); lPointRearTopRight(); lPointRearBottomRight(); lPointRearBottomLeft();

      // top square
      lPointFrontTopLeft(); lPointFrontTopRight(); lPointRearTopRight(); lPointRearTopLeft();

      // bottom square
      lPointFrontBottomLeft(); lPointFrontBottomRight(); lPointRearBottomRight(); lPointRearBottomLeft();

      // left square
      lPointFrontTopLeft(); lPointRearTopLeft(); lPointRearBottomLeft(); lPointFrontBottomLeft();

      // right square
      lPointFrontTopRight(); lPointRearTopRight(); lPointRearBottomRight(); lPointFrontBottomRight();

      GL.End();
    } //

    private void DrawTetrahedron(System.Drawing.Color xColor, System.Drawing.Color xColor2, float X, float Y, float Z, float xSideLength) {
      float lDistMidToVertex = (float)Math.Sqrt(6.0) / 4f * xSideLength;
      float lDistMidToFloor = (float)Math.Sqrt(6.0) / 12f * xSideLength;
      float lHeight = (float)Math.Sqrt(2.0 / 3.0) * xSideLength; // = lDistMidToVertex + lDistMidToEdge
      float lTop = Y + lDistMidToVertex;
      float lBottom = Y - lDistMidToFloor;
      float lRight = X + xSideLength / 2f;
      float lLeft = X - xSideLength / 2f;
      float lRear = Z - (float) (xSideLength * Math.Sqrt(3.0) / 3.0);
      float lFront = Z + (float)(xSideLength * Math.Sqrt(3.0) / 6.0);

      GL.Begin(PrimitiveType.Triangles);

      Action lPointTop = () => { GL.Color3(xColor); GL.Vertex3(X, lTop, Z); };
      Action lPointFrontBottomLeft = () => { GL.Color3(xColor2); GL.Vertex3(lLeft, lBottom, lFront); };
      Action lPointFrontBottomRight = () => { GL.Color3(xColor); GL.Vertex3(lRight, lBottom, lFront); };
      Action lPointRear = () => { GL.Color3(xColor2); GL.Vertex3(X, lBottom, lRear); };

      // front triangle
      lPointTop(); lPointFrontBottomLeft(); lPointFrontBottomRight();

      // left triangle
      lPointTop(); lPointFrontBottomLeft(); lPointRear();

      // right triangle
      lPointTop(); lPointFrontBottomRight(); lPointRear();

      // bottom triangle
      lPointFrontBottomLeft(); lPointFrontBottomRight(); lPointRear();

      GL.End();
    } //

  } // class
} // namespace

 

About Bastian M.K. Ohta

Happiness only real when shared.

Posted on June 10, 2015, in Advanced, C#, WPF and tagged , , , , , , , , , , . Bookmark the permalink. Leave a comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: