Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iOS

Intel's Multi-OS Engine - OpenGL sample

5.00/5 (1 vote)
14 Jan 2016CPOL5 min read 11.7K  
This article describes the OpenGLBox sample in the Multi-OS Engine.

This article is for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers

Intel® Developer Zone offers tools and how-to information for cross-platform app development, platform and technology information, code samples, and peer expertise to help developers innovate and succeed. Join our communities for Android, Internet of Things, Intel® RealSense™ Technology, and Windows to download tools, access dev kits, share ideas with like-minded developers, and participate in hackathon’s, contests, roadshows, and local events.

This article describes the OpenGLBox sample in the Multi-OS Engine. This is a truly cross-platform sample. On the screenshots below you can see that the same Java-based application on iOS and Android looks and behaves the same way. Applications for different platforms share the data and part of the source code. How is this achieved? Scroll down!

Every single OpenGL programmer has, at least one time, made this small program: the rotating 3D cube. Don't be afraid If you are not so familiar with 3D graphics programming. This example will help you to start from scratch. If you are already good at OpenGL, this article will help you to focus on the specific features of Android and iOS development. Let's begin.

Our geometry contains 6 faces, as befits a cube, and 24 vertices. Why are so many vertices? Each face has a texture of Multi-OS Engine logo. Thus, we have to divide each of 8 real vertices into 3 new vertices with different texture coordinates.

There is no lighting effects in our scene. So we are able to use pretty simple shaders. The vertex shader just transforms geometry and passes the texture coordinates to the fragment shader. Then the fragment shader makes simple texturing.

Platform specific code

Multi-OS Engine does not try to hide platform-specific code unnecessarily. It's great for porting and debugging applications. The classes for each platform can have their own features:

iOS
package com.intel.inde.moe.samples.openglbox.ios;

class Main extends NSObject implements UIApplicationDelegate
class OpenGLBoxController extends GLKViewController implements GLKViewDelegate
class ShaderProgram extends ShaderProgramBase

The GLKit framework provides functions and classes that reduce the effort required to create new shader-based apps:

GLKViewController

The GLKViewController class provides all of the standard view controller functionality, but additionally implements an OpenGL ES rendering loop. A GLKViewController object works in conjunction with a GLKView object to display frames of animation in the view.

GLKView

The GLKView class simplifies the effort required to create an OpenGL ES application by providing a default implementation of an OpenGL ES-aware view. A GLKView directly manages a framebuffer object on your application’s behalf; your application simply draws into the framebuffer when the contents need to be updated.

GLKViewDelegate

An object that implements the GLKViewDelegate protocol can be set as a GLKView object’s delegate. A delegate allows your application to provide a drawing method to a GLKView object without subclassing the GLKView class.

Android
package com.intel.inde.moe.samples.openglbox.android;

class OpenGLBoxActivity extends Activity
class SurfaceView extends GLSurfaceView
class BoxRenderer implements GLSurfaceView.Renderer
class ShaderProgram extends ShaderProgramBase

There are two foundational classes in the Android framework that let you create and manipulate graphics with the OpenGL ES API: GLSurfaceView and GLSurfaceView.Renderer. To use OpenGL in your Android application, one needs to understand how to implement these classes in an activity.

GLSurfaceView

This class is a View where you can draw and manipulate objects using OpenGL API calls and is similar in function to a SurfaceView. You can use this class by creating an instance of GLSurfaceView and adding your Renderer to it. However, if you want to capture touch screen events, you should extend the GLSurfaceView class to implement the touch listeners responding to Touch Events.

GLSurfaceView.Renderer

This interface defines the methods required for drawing graphics in a GLSurfaceView. You must provide an implementation of this interface as a separate class and attach it to your GLSurfaceView instance using GLSurfaceView.setRenderer().

Common code

OpenGL-code is similar on both platforms. It allows us to take out interfaces or even abstract classes into the common part of the project. For example, a class working with shaders must inherit abstract base class <a id="ShaderProgramBase" name="ShaderProgramBase">ShaderProgramBase</a>:

Java
package com.intel.inde.moe.samples.openglbox.common;

public abstract class ShaderProgramBase {

    protected static int INVALID_VALUE = -1;
    protected int programHandle = INVALID_VALUE;
    protected HashMap<String, Integer> attributes = new HashMap<String, Integer>();

    public abstract void create(String vertexCode, String fragmentCode);
    public abstract void use();
    public abstract void unUse();
    public abstract int getAttributeLocation(String attribute);
    public abstract int getUniformLocation(String uniform);

    protected abstract int loadShader(int type, String shaderCode);

    public int getProgramHandle() {
        return programHandle;
    }
}

Now let's dive into platform-specific implementations of ShaderProgramBase.create() method.

iOS

Java
package com.intel.inde.moe.samples.openglbox.ios;
...

public class ShaderProgram extends ShaderProgramBase {
...

    @Override
    public void create(String vertexCode, String fragmentCode) {
        ...

        programHandle = OpenGLES.glCreateProgram();
        OpenGLES.glAttachShader(programHandle, vertexShader);
        OpenGLES.glAttachShader(programHandle, fragmentShader);
        OpenGLES.glLinkProgram(programHandle);
    }
}

Android

Java
package com.intel.inde.moe.samples.openglbox.android;
...

public class ShaderProgram extends ShaderProgramBase {
...

    @Override
    public void create(String vertexCode, String fragmentCode) {
        ...

        programHandle = GLES20.glCreateProgram();
        GLES20.glAttachShader(programHandle, vertexShader);
        GLES20.glAttachShader(programHandle, fragmentShader);
        GLES20.glLinkProgram(programHandle);
    }
}

As you can see these code snippets look pretty similar. It may seem that the code differs only in prefixes - OpenGLES and GLES20. Not quite right but not far off. Look carefully at loadShader() method:

iOS

Java
package com.intel.inde.moe.samples.openglbox.ios;
...

public class ShaderProgram extends ShaderProgramBase {
...

    @Override
    protected int loadShader(int type, String shaderCode) {
        ...

        IntPtr status = PtrFactory.newIntReference();
        OpenGLES.glGetShaderiv(shader, ES2.GL_COMPILE_STATUS, status);
        if (status.getValue() == 0) {
            BytePtr info = PtrFactory.newWeakByteArray(256);
            OpenGLES.glGetShaderInfoLog(shader, 256, PtrFactory.newWeakIntReference(0), info);
            OpenGLES.glDeleteShader(shader);
            throw new IllegalArgumentException("Shader compilation failed with: " + info.toASCIIString());
        }
        ...
    }
}

Android

Java
package com.intel.inde.moe.samples.openglbox.android;
...

public class ShaderProgram extends ShaderProgramBase {
...

    @Override
    protected int loadShader(int type, String shaderCode) {
        ...

        int[] status = new int[1];
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] == 0) {
            String info = GLES20.glGetShaderInfoLog(shader);
            GLES20.glDeleteShader(shader);
            throw new IllegalArgumentException("Shader compilation failed with: " + info);
        }
        ...
    }
}

This code differs by the way we pass a pointers to OpenGL. You will see similar distinctions in other places too.

Common data

Now it's time to describe other common parts. Our applications uses the following general data:

  1. Geometry
    Java
    public class Geometry {
        public static final float[] VERTICES = new float[] { … };
        public static final byte[] INDICES = { … };
    }
  2. Shaders (as String constants)
    Java
    public class Shaders {
        public static final String VERTEXT_SHADER = " ... ";
        public static final String FRAGMENT_SHADER = " ... ";
    }
  3. Parameters (background color, rotation speed, different camera related values)
    Java
    public class Parameters {
        ...
        public static final float DEGREES_PER_SECOND = 90.0f;
        ...
    }

All this constants are self-explained. They represent the common 3D data model and operational logics. In larger projects the common data will be even greater.

3D Mathematics

OpenGL ES 2.0 and later doesn’t provide built-in functions for creating or specifying transformation matrices. Instead, programmable shaders provide vertex transformation, and you specify shader inputs using generic uniform variables.

iOS

The GLKit framework includes a comprehensive library of vector and matrix types and functions, optimized for high performance on iOS hardware.

Java
viewMatrix = GLKit.GLKMatrix4MakeLookAt(
    0.0f, 0.0f, Parameters.EYE_Z, // eye
    0.0f, 0.0f, 0.0f, // center
    0.0f, 1.0f, 0.0f // up-vector
);
viewMatrix = GLKit.GLKMatrix4Rotate(viewMatrix,
    GLKit.GLKMathDegreesToRadians(Parameters.PITCH), 1, 0, 0);

Android

Class android.opengl.Matrix provides math utilities. These methods operate on OpenGL ES format matrices and vectors stored in float arrays.

Java
Matrix.setLookAtM(viewMatrix, 0,
    0.0f, 0.0f, Parameters.EYE_Z, // eye
    0.0f, 0.0f, 0.0f, // center
    0.0f, 1.0f, 0.0f // up-vector
);
Matrix.rotateM(viewMatrix, 0, Parameters.PITCH, 1, 0, 0);

But you can implement basic math classes on your own and increase the reuse of the common code.

Loading textures

iOS

The GLKTextureLoader class simplifies the effort required to load your texture data. The GLKTextureLoader class can load two-dimensional or cubemap textures in most image formats supported by the Image I/O framework. On iOS, it can also load textures compressed in the pvrtc format. It can load the data synchronously or asynchronously.

Java
public static int loadGLTexture(String name, String extension) {
    NSMutableDictionary options = NSMutableDictionary.alloc().init();
    options.put(NSNumber.numberWithBool(true), GLKit.GLKTextureLoaderOriginBottomLeft());

    String path = NSBundle.mainBundle().pathForResourceOfType(name, extension);
    GLKTextureInfo info = GLKTextureLoader.textureWithContentsOfFileOptionsError(path, options, null);
    if (info == null) {
        System.out.println("Error loading file: " + name + "." + extension);
        return -1;
    }

    return info.name();
}

The GLKTextureLoader and GLKTextureInfo classes do not manage the OpenGL texture for you. Once the texture is returned to your app, you are responsible for it. This means that after your app is finished using an OpenGL texture, it must explicitly deallocate it by calling the glDeleteTextures() function.

Android

For good or bad, but we're implementing it manually:

Java
public static int loadGLTexture(Bitmap bitmap) {
    int[] textureIDs = new int[1];
    GLES20.glGenTextures(1, textureIDs, 0);

    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIDs[0]);

    // Create Nearest Filtered Texture
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
            GLES20.GL_LINEAR);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
            GLES20.GL_LINEAR);

    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

    // Use the Android GLUtils to specify a two-dimensional texture image from our bitmap
    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

    return textureIDs[0];
}

Of course, you always can write your own wrapper for such OpenGL-intensive code. We didn't set to ourselves such a task.

Timing

iOS

Method timeSinceLastUpdate() returns the amount of time that has passed since the last time the view controller called the delegate’s update() method:

Java
public void update() {
    rotation += Parameters.DEGREES_PER_SECOND * timeSinceLastUpdate();
    modelMatrix = GLKit.GLKMatrix4Identity();
    modelMatrix = GLKit.GLKMatrix4Rotate(modelMatrix, GLKit.GLKMathDegreesToRadians(rotation), 0, 1, 0);
}

Android

There is no such method on Android. So we have to recreate it manually:

Java
private long lastTime;

public void onSurfaceCreated(GL10 unused, EGLConfig config) {
    …
    lastTime = System.nanoTime();
}

public void onDrawFrame(GL10 unused) {
    update();
    drawBox();
}

double timeSinceLastUpdate() {
    long time = System.nanoTime();
    double delta = (double) (time - lastTime) / 1000000000;
    lastTime = time;
    return delta;
}

private void update() {
    if (isPaused)
        return;
    rotation += Parameters.DEGREES_PER_SECOND * timeSinceLastUpdate();
    Matrix.setIdentityM(modelMatrix, 0);
    Matrix.rotateM(modelMatrix, 0, rotation, 0, 1, 0);
}

Summary

OpenGL development with Java on iOS is quite similar to that on Android with many advantages:

  • OpenGL ES is a C-based API that is supported by both iOS and Android with great portability
  • Familiar Java syntax
  • Debugging two targets side by side use the powerful iOS simulator and Android emulator

License

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