Native 2D Rendering In An Eclipse View Using OpenGL

I’ve been learning how to render graphics in an Eclipse view using OpenGL by putting together (hacking) bits and pieces from various articles I’ve found on the web.  I have enough working to share what I’ve learned and show you how to natively draw basic 2D graphics.  The end result will look like this …

End Result

SWT provides a GLCanvas widget for displaying graphics rendered using OpenGL but does not provide the JNI code that interfaces to the OpenGL library to actually draw the graphics.  LWJGL is a JNI library that interfaces to OpenGL and can be installed as an Eclipse bundle from the LWJGL update site.  Another option is to write your own JNI to interface to OpenGL and do the rendering natively.  I have chosen the later path since LWJGL is not available for all platforms and I had a use-case where I needed to interface to an existing C library to retrieve the data to be rendered.  Writing the JNI is not all that hard as there are only a couple of functions that are needed to do basic rendering.

Let’s start by creating a new C++ project called com.example.opengl.native.  Set the Project type to Shared Library -> Empty Project and Toolchains to MacOSX GCC.

C++ ProjectAfter the project is created, bring up the project properties.  We need to tell the linker where to fine the OpenGL library, and the compiler where to find the JNI and OpenGL headers.  Navigate to the C/C++ Build -> Settings section and under MacOS X C++ Linker -> Miscellaneous settings, add -framework OpenGL to the Linker flags.  The -framework option is a OS X specific GCC option that tells the linker which OS X frameworks (shared libraries) to link against.  If you are not using OS X, then add the appropriate -L and -l settings for your platform under the Libraries section.

Linker Flags

Next, in the GCC C++ Compiler -> Directories settings add /System/Library/Frameworks/JavaVM.framework/Headers and /System/Library/Frameworks/OpenGL.framework as part of the include paths.  For other platforms add the appropriate directories containing the JRE and OpenGL header files.

Header Dirs

Finally, switch to the Build Artifact tab and change the Artifact name to opengl and the Artifact extension to jnilib.  Shared libraries in OS X typically have the extension .dylib; however, JNI libraries have a special .jnilib extension.  For other platforms, the default shared library extension is probably fine for JNI (.so for Linux as an example).

Build Artifacts

Now, let’s create a plug-in project called com.example.opengl.  You do not need to create an activator for this bundle.  First, we are going to create a class to hold the native functions that interface to OpenGL.

package com.example.opengl;

public class OpenGL
{
	static
	{
		System.loadLibrary("opengl");
	}

	public static native void glClearColor(float red, float green, float blue, float alpha);
	public static native void glOrtho(double left, double right, double bottom, double top, double near, double far);
	public static native void glTranslatef(float x, float y, float z);
	public static native void glViewport(int x, int y, int width, int height);

	public static native void init2D();
}

Once we have the native functions declared, we need to create the corresponding header (.h) file in the C++ project using the javah program that comes with the JDK.  You can do this right from Eclipse by creating an external program launch configuration.  Set up the launch configuration on the Main tab as follows:

  • Name: OpenGL JNI
  • Location: ${system_path:javah}
  • Working directory: ${workspace_loc:com.example.opengl.native}
  • Arguments: -classpath ${workspace_loc:com.example.opengl/bin} com.example.opengl.OpenGL

OpenGL JNI MainOn the Refresh tab, select Specific resources, click Specify Resources… and check com.example.opengl.native.  This will cause the C++ project to be refreshed after the header file is created.

OpenGL JNI Refresh

Finally, click Run to execute javah and place the header file in the C++ project.  If you expand the C++ project in the Project Explorer, you will see that the header file com_example_opengl_OpenGL.h was created.  In the C++ project create a source file named com_example_opengl_OpenGL.cpp.

#include "com_example_opengl_OpenGL.h"
#include "gl.h"

void JNICALL Java_com_example_opengl_OpenGL_glClearColor(JNIEnv* env, jclass clazz, jfloat red, jfloat green, jfloat blue, jfloat alpha)
{
  glClearColor(red, green, blue, alpha);
}

void JNICALL Java_com_example_opengl_OpenGL_glOrtho(JNIEnv* env, jclass clazz, jdouble left, jdouble right, jdouble bottom, jdouble top, jdouble near, jdouble far)
{
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity();
  glOrtho(left, right, bottom, top, near, far);
  glMatrixMode (GL_MODELVIEW);
  glLoadIdentity();
}

void JNICALL Java_com_example_opengl_OpenGL_glTranslatef(JNIEnv* env, jclass clazz, jfloat x, jfloat y, jfloat z)
{
  glTranslatef(x, y, z);
}

void JNICALL Java_com_example_opengl_OpenGL_glViewport(JNIEnv* env, jclass clazz, jint x, jint y, jint width, jint height)
{
  glViewport(x, y, width, height);
}

void JNICALL Java_com_example_opengl_OpenGL_init2D(JNIEnv* env, jclass clazz)
{
  glDisable(GL_DEPTH_TEST);
}

As you can see, the implementation of the JNI is almost a 1:1 mapping to the OpenGL API. I did a little extra work in glOrtho() which you could break out into separate functions if you prefer.  Now, let’s create the native rendering engine to draw a rectangle in OpenGL.

package com.example.opengl;

public class RenderEngine
{
	public native void draw();
}

Just like you did for the OpenGL class, create an external program launch configuration to run javah and create the header file in the C++ project.  Set up the launch configuration on the Main tab as follows:

  • Name: RenderEngine JNI
  • Location: ${system_path:javah}
  • Working directory: ${workspace_loc:com.example.opengl.native}
  • Arguments: -classpath ${workspace_loc:com.example.opengl/bin} com.example.opengl.RenderEngine

RenderEngine JNI MainClick Run and the com_example_opengl_RenderEngine.h header file will be generated in the C++ project.  Switch to the C++ project and implement the draw() function by creating the source file com_example_opengl_RenderEngine.cpp. The implementation draws a series of lines to form a rectangle. I’ll explain the negative y axis in a minute.  This is all of the native code so you can build the C++ project which will result in a libopengl.jnilib in the Debug folder.

#include "com_example_opengl_RenderEngine.h"
#include "gl.h"

void JNICALL Java_com_example_opengl_RenderEngine_draw(JNIEnv* env, jobject object)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3ub(255, 255, 255);
  glBegin(GL_LINE_LOOP);
  {
    glVertex2d(10, -10);
    glVertex2d(150, -10);
    glVertex2d(150, -40);
    glVertex2d(10, -40);
  }
  glEnd();
}

Now that we have code that interfaces to OpenGL and draws a rectangle, let’s put it all together in an Eclipse view. In the Java plug-in project, create a new extension selecting org.eclipse.ui.views as the extension point. Set the following fields in the extension:

  • id: com.example.opengl.view
  • class: com.example.opengl.OpenGLView
  • name: OpenGL

Implement the OpenGLView as:

package com.example.opengl;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.opengl.GLData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.ViewPart;

public class OpenGLView extends ViewPart
{
	@Override
	public void createPartControl(Composite parent)
	{
		parent.setLayout(new FillLayout());
		GLData glData = new GLData();
		glData.doubleBuffer = true;
		canvas = new GLCanvas(parent, SWT.H_SCROLL | SWT.V_SCROLL, glData);
		canvas.getHorizontalBar().setMaximum(2000);
		canvas.getVerticalBar().setMaximum(2000);

		renderEngine = new RenderEngine();

		Display.getDefault().asyncExec(new Runnable()
		{
			public void run()
			{
				canvas.setCurrent();
				OpenGL.init2D();
				OpenGL.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
			}
		});

		canvas.addControlListener(new ControlAdapter()
		{

			@Override
			public void controlResized(ControlEvent e)
			{
				redraw();
			}
		});

		canvas.getHorizontalBar().addSelectionListener(new SelectionAdapter()
		{
			@Override
			public void widgetSelected(SelectionEvent e)
			{
				redraw();
			}
		});

		canvas.getVerticalBar().addSelectionListener(new SelectionAdapter()
		{
			@Override
			public void widgetSelected(SelectionEvent e)
			{
				redraw();
			}
		});
	}

	@Override
	public void setFocus()
	{
		canvas.setFocus();
	}

	private void redraw()
	{
		Display.getDefault().asyncExec(new Runnable()
		{
			public void run()
			{
				Rectangle bounds = canvas.getClientArea();
				canvas.setCurrent();
				int xOffset = canvas.getHorizontalBar().getSelection();
				int yOffset = canvas.getVerticalBar().getSelection();
				OpenGL.glViewport(bounds.x, bounds.y, bounds.width, bounds.height);
				OpenGL.glOrtho(bounds.x + xOffset, bounds.width + xOffset, -bounds.height - yOffset, bounds.y - yOffset, 0.0, 1.0);
				OpenGL.glTranslatef(0.375f, 0.375f, 0.0f);
				renderEngine.draw();
				canvas.swapBuffers();
			}
		});
	}

	private GLCanvas canvas;
	private RenderEngine renderEngine;
}

Notice that all of the calls to OpenGL are made inside a Display.getDefault().asyncExec() call. Why is this necessary when the code is already running in the UI thread? This is the one thing that I don’t understand in this example. If anyone has an explanation, please comment and I’ll update the entry. The call to OpenGL.glViewport() is only necessary when the view is resized – it was just easier to have one redraw() function.  The call to OpenGL.glOrtho() does most of the interesting work.  It sets the orthographic projection of the 3D space into 2D space which translates to pan, zoom, and the axes origin and orientation in 2D.  The arguments to the function are: left, right, bottom, top, near, and far.  I will discuss each of the arguments in detail, but first, a word on the axes origin and orientation.

OpenGL by default orients the axes with positive x to the right, negative x to the left, positive y up, negative y down, positive z out of the display, and negative z into the display.  By specifying positive or negative values for the arguments to glOrtho(), you can change the orientation of the axes.  The absolute values of the arguments specify the location of the origin.  I’ve set the axes origin to be the upper left corder of the canvas (view), and it is oriented such that positive x is to the right and negative y is down.  It is possible to orient the axes such that positive y is down making 2D drawing match what you are used to but when I tried this, text rendered using GLUT was upside down.  Rather than trying to figure out if there was a way to flip the text, I found it much easier to draw in negative y.  I set the scale to be 1:1 in pixels, meaning that if the canvas is 1000 pixels wide, drawing at x = 1000 will draw on the right edge of the canvas.  Now, let’s look at the glOrtho() arguments:

Left: bounds.x + xOffset

This argument sets the left clipping plane which effectively sets the origin of the x axis relative to the left edge of the viewport.  The xOffset takes into account the position of the horizontal scrollbar.  If the scrollbar is all the way to the left, then you will see what was drawn starting at x = 0 and extending into positive x.

Right: bounds.width + xOffset

This argument sets the right clipping plane which effectively sets the x scale relative to the viewport.  The xOffset takes into account the position of the horizontal scrollbar so that the scale doesn’t change when you pan.  If the scrollbar is all the way to the left, then you will see what was drawn from x = 0 to x = bounds.width.

Bottom: -bounds.height – yOffset

This argument sets the bottom clipping plane which effectively sets the y scale relative to the viewport.  The yOffset takes into account the position of the vertical scrollbar so that the scale doesn’t change when you pan.  If the scrollbar is all the way to the top, then you will see what was drawn from y = 0 to y = -bounds.height.

Top: bounds.y – yOffset

This argument sets the top clipping plane which effectively sets the origin of the y axis relative to the top edge of the viewport.  The yOffset takes into account the position of the vertical scrollbar.  If the scrollbar is all the way to the top, then you will see what was drawn starting at y = 0 and extending into negative y.

Near: 0.0

Far: 1.0

These arguments set the values of the near and far clipping planes.  Since the z axis is not used in 2D, the examples I found all set the near clipping plane to 0.0, and the fat clipping plane to 1.0.

As you move the scrollbar to the right, both the left and right clipping planes move which pans the drawn area in the x axis.  As you move the scrollbar to the bottom, both the top and bottom clipping planes move which pans the drawn area in the y axis.  One thing I have not done in this example is to take into account the size of the scrollbar thumb.  You may have to adjust the right and bottom arguments to glOrtho() taking into account scrollbar.getThumb().

The call to OpenGL.glTranslatef(0.375f, 0.375f, 0.0f) is a “trick” necessary to make your drawing pixel accurate.  Try drawing a line from (0, -10) to (10, -10) and a line from (0, -20) to (0, -30) without the call to glTranslate().  The result will not be what you expect.

Before we can launch the Eclipse runtime workbench, we need to hook the JNI library into the plug-in.  We are going to bundle the native library with the plug-in so that it can be delivered as a single unit. In the plug-in project, create a lib folder at the root of the project.  Copy the libopengl.jnilib file from the Debug folder in the C++ project to the lib folder in the plug-in project.  Edit the build.properties to include the newly created lib folder along with its contents.  Finally, edit the MANIFEST.MF of the plug-in project and using the source tab of the editor, add the line:

Bundle-NativeCode: lib/libopengl.jnilib; osname=macosx; processor=x86

This tells the class loader where to find the native library when System.loadLibrary() is called.  Now, we can create an Eclipse Application launch configuration to run the runtime workbench.  You can use the defaults for the launch configuration.  After you launch the runtime workbench, select Window -> Show View -> Other… and under the Other category, select the OpenGL view and click OK.  You should see the view with a white rectangle drawn on a black background just like the first screenshot above.

Advertisements

3 thoughts on “Native 2D Rendering In An Eclipse View Using OpenGL

  1. Nice post. The JNI part is very interesting.

    > LWJGL is not available for all platforms
    Which unsupported platform are you refering to ?
    I use LWJGL for an RCP application (ShareMedia) and it runs at least on Win32, Linux 32/64 and OSX Carbon 32/ Cocoa 32/64.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s