If you’ve been following my tutorials on OpenGL ES 2 for iOS, by the time you finish Texturing (Part 6 or so) you’ll have a lot of code crammed into a single UIViewController. This is intentional: the tutorials are largely self-contained, and only create classes and objects where OpenGL itself uses class-like-things.
…but most of the code is re-used from tutorial to tutorial, and it’s getting in the way. It’s time for some refactoring.
UIViewController vs GLKViewController
Recap: Both these classes are provided by Apple; GLKViewController is a UIViewController that’s been modified for OpenGL apps. It has the simplest possible interface – a single “update” method that you override, which is called (automatically) once per frame.
As I explained in the first GL ES 2 tutorial, everything in OpenGL revolves around the Draw call. Most engines recognize this, so we’ll subclass GLKViewController and replace:
- Once per frame: call “update”; no intelligence
…with:
- In viewDidLoad: Correct setup of EAGLContext (required)
- In viewDidLoad: Pre-Create all the Draw calls needed for the app
- Once per frame: For each Draw call in the list:
- …update all the OpenGL per-Draw settings
- …update the OpenGL Shader settings
- …render the Draw call
- …reset any OpenGL state that “must” be turned off immediately after use (e.g. GL_SCISSOR)
Most of this is identical for all apps – we can create a single base class “GLK2DrawCallViewController”, and put the code in there. A few items will be unique from app to app (e.g. the list of Draw calls actually rendered!); we do those with their own public methods so that you can write a subclass per-app which only overrides the methods it needs to change.
GLK2DrawCallViewController.h:
@interface GLK2DrawCallViewController : GLKViewController @property(nonatomic,retain) EAGLContext* localContext; @property(nonatomic, retain) NSMutableArray* drawCalls;
This class manages the EAGLContext correctly, and populates the drawCalls – most subclasses can ignore these properties. But some special effects will require you to access the super-class’s data here.
-(NSMutableArray*) createAllDrawCalls; -(void) willRenderFrame; -(void) willRenderDrawCallUsingVAOShaderProgramAndDefaultUniforms:(GLK2DrawCall*) drawCall; @end
One callback to create the Draw calls – this is essential, if you don’t implement it, nothing will be drawn.
The other two are optional, although the one about “willRender….Using…Uniforms” is needed for most non-trivial shaders (any shader where a Uniform is changing from frame to frame – e.g. whenever your 3D objects are moving).
Internally, it’s all existing code that I’ve simply moved around.
Usage
Usage is exceptionally simple: change your ViewController.h in your app project to extend the new class:
ViewController.h:
@interface ViewController : GLK2DrawCallViewController
…and override the special method for creating Draw calls. Move all your tutorial / logic there:
ViewController.h:
-(NSMutableArray*) createAllDrawCalls { /** All the local setup for the ViewController */ NSMutableArray* result = [NSMutableArray array]; /** -- Draw Call 1: clear the background */ GLK2DrawCall* simpleClearingCall = [[GLK2DrawCall new] autorelease]; simpleClearingCall.shouldClearColorBit = TRUE; [simpleClearingCall setClearColourRed:0.5 green:0 blue:0 alpha:1]; [result addObject: simpleClearingCall]; ... etc return result; }
Source files on Github:
Re-using the “draw one triangle” code
The tutorials frequently use “draw a single triangle” to demonstrate new code. It would be great to re-use this code – we use it a lot for debugging, not just for tutorials. But it can’t live inside the OpenGL API: to keep the API small and easy to learn, it only includes commands for talking to the GPU efficiently.
With desktop GL they solved this problem by creating a separate, “optional” library called GLU. Because GLU is purely software – it has no access to hardware, instead using GL to do all the GPU work – you can copy/paste GLU code and use it with different versions of GL, often with little or no changes.
NOTE: I’m not including this code in the GLKit-Extended library – it’s stuff that would be written differently for different 3D engines, and I want the library to be pure and simple. Instead, I’m including it in the GLKit-Extended Demo project, and you can copy/paste the file direct to your own projects if you choose
Sadly, most GLU code won’t work with GL ES unless you tweak it – GLU uses lots of outdated commands that were stripped to make GL ES cheaper to implement. But we can copy the idea…
CommonGLEngineCode.h:
@interface CommonGLEngineCode : NSObject +(GLK2DrawCall*) drawCallWithUnitTriangleAtOriginUsingShaders:(GLK2ShaderProgram*) shaderProgram; +(GLK2DrawCall*) drawCallWithUnitCubeAtOriginUsingShaders:(GLK2ShaderProgram*) shaderProgram; ... over time, we'll add more and more "re-usable" methods here... @end
Each method assumes that you’ve NOT CHANGED ANYTHING in the camera-setup of OpenGL – you’re using an unconfigured, default frustum etc. i.e. the projection is Orthogonal, and only shows things from (-1,-1,-1) to (1,1,1). It sticks a 1-unit wide triangle (or cube) roughly in the middle of the screen.
If the methods were 100% generic, they’d take another argument: a dictionary of GLK2Attributes, each one “tagged” so the method knows which ones to fill with 3D positions, which to fill with 2D texture co-ordinates, etc. For simplicity, I’ve assumed that you pass in a pre-compiled GLK2ShaderProgram containing a shader pair with:
- REQUIRED: an Attribute named “position” which you’ll read to get the 3D position of each vertex
- OPTIONAL: an Attribute named “textureCoordinate” which you’ll read if you’re using Texturing to get a texture-co-ordinate ranging (0-1) in X (i.e. “u”) and (0-1) in Y (i.e. “v”)
Usage
Combining this with the new GLK2DrawCallViewController above, we get something like:
ViewController.h:
-(NSMutableArray*) createAllDrawCalls { /** All the local setup for the ViewController */ NSMutableArray* result = [NSMutableArray array]; GLK2DrawCall* dcTri = [CommonGLEngineCode drawCallWithUnitTriangleAtOriginUsingShaders: [GLK2ShaderProgram shaderProgramFromVertexFilename:@"VertexProjectedWithTexture" fragmentFilename:@"FragmentWithTexture"]]; GLK2Uniform* samplerTexture1 = [dcTri.shaderProgram uniformNamed:@"s_texture1"]; ...OPTIONAL (if you're using texture-mapping in your shaders): GLK2Texture* texture = [GLK2Texture textureNamed:@"tex2"]; [dcTri setTexture:texture forSampler:samplerTexture1]; [result addObject:dcTri]; ... return result; }
Source files on Github: