Executive summary: This article demonstrates the use of Objective-C’s dynamic object model and the Foundation framework to extract attributes from objects using information not available at compile time. This is done on the context of implementing OpenGL Shader Language support in a cross-platform game.
Introduction
A major feature of the current development line of Oolite is support for GLSL shaders. Shaders need to be able to reflect the state of the object they’re attached to – for instance, by having spaceship engines glow in proportion to engine power. The mechanism GLSL provides for this is uniform variables, attributes set by the host application and read by the shader.
The initial implementation of shader support was quite simplistic:
for (each material in model) { if (material is shader-based) { glUseProgramObjectARB(shader); if (shader uses uniform:"time") { glUniform1fARB(location for "time", current game time); } if (shader uses uniform:"engine_level") { glUniform1fARB(location for "engine_level", ship’s engine power level); } … } else { glUseProgramObjectARB(NULL); set up texture; } render geometry for this material; }
The problem
This gets the job done, but it isn’t exactly a paragon of object-oriented design. There’s no encapsulation and no reuse. It’s definitely not very Cocoa-ey. The first step was obviously to make materials into a class hierarchy, with simple texture-based materials being one class, shader materials being another and the rendering code not really caring which was being used. However, if the shader material class set uniforms in the way outlined above, it would require the shader material class to know about the details of the various types of entity in the game, and special-case each one. Conversely, if the responsibility for setting up uniforms remained in the entity classes, they would need to know about the details of shaders.
So how does Cocoa handle comparable situations? Key-value coding. Key-value observation. Bindings.
As it stands, KVC doesn’t really suit our needs. Accessors for relevant properties tend not to return objects. Properties of potential interest to shaders tend to be numbers, vectors, matrices, quaternions (Oolite will convert quaternions to either vectors or rotation matrices for shader bindings) and colours. Colours are represented by objects, but the others are generally not. So I decided to roll my own.
Getting the goods
This turned out to be almost disappointingly simple, although there was a complication arising from the need to support GNUstep as well as Cocoa. First, in order to support multiple types, we need to use -[NSObject methodSignatureForSelector:]
to check the return type of the requested method. Then (assuming the type is supported, and other sanity checks), -[NSObject methodForSelector:]
, which returns an IMP
, or function pointer to the method implementation. This function pointer can then be cast to a function pointer with the correct return type, and called to acquire the current value of the attribute in question.
Note: there are some limitations to this approach. In particular, it shortcuts certain dynamic method look-up behaviours. It will not work if the object being bound to (the binding target) is a proxy, and it will not notice if the object’s class changes, or the class’s method table changes. Such changes are made by the key-value observation mechanism used for normal Cocoa bindings. This doesn’t matter in Oolite, and probably won’t matter in the KVO case either, but it is important to be aware of it.
- (BOOL)setBindingTarget:(id)target selector:(SEL)selector { NSMethodSignature *signature; IMP method; unsigned argCount; ShaderUniformType type; if (target == nil) return NO; if (![target respondsToSelector:selector]) return NO; // Get the IMP method = [target methodForSelector:selector]; if (method == NULL) return NO; // Get the method signature signature = [target methodSignatureForSelector:selector]; if (signature == nil) return NO; // All methods have two implicit arguments: self and _msg. // Getters have no explicit arguments and therefore have // a total of two arguments. argCount = [signature numberOfArguments]; if (argCount != 2) return NO; type = ShaderUniformTypeFromMethodSignature(signature); if (type == kShaderUniformTypeInvalid) return NO; // All tests passed – binding is complete. _target = target; _selector = selector; _method = method; _type = type; } typedef float (*FloatReturnMsgSend)(id, SEL); - (void)apply { switch (_type) { case kShaderUniformTypeFloat: float fValue = ((FloatReturnMsgSend)_method)(_target, _selector); glUniform1fARB(_location, fValue); break; // Handle other types … } }
But, er… what is it?
The only non-standard thing in the above is the function ShaderUniformTypeFromMethodSignature()
. It takes an NSMethodSignature
and returns an enum
specifying the return type of the accessor method we are binding to. The method -[NSMethodSignature methodReturnType]
exists for this very purpose. However, this is the source of the aforementioned complication. Cocoa’s implementation returns what you might expect: the @encode()
string corresponding to the method’s return type. Therefore, the initial implementation of ShaderUniformTypeFromMethodSignature()
in Oolite looked like this:
ShaderUniformType ShaderUniformTypeFromMethodSignature(NSMethodSignature *sig) { const char *type = [sig methodReturnType]; if (type == NULL) return kShaderUniformTypeInvalid; if (strcmp(type, @encode(float)) == 0) return kShaderUniformTypeFloat; // Handle other types … else return kShaderUniformTypeInvalid; }
However, as the astute reader may have guessed, this did not work under GNUstep. Assiduous link-followers will be quick to point out that the documentation clearly states, “This encoding is implementation-specific, so applications should use it with caution.” In fact, GNUstep seems to return an encoding string for the entire method, that is, the encoding of all the arguments as well as the return type. (I maintain that this is a bug, since it returns different strings for -(int)foo
and -(int)bar:(id)frob
. However, since we’re only interested in methods with no explicit arguments, that doesn’t matter.) More importantly to the Cocoa programmer, this statement in the documentation means Apple is free to change the Cocoa implementation at any time.
Fortunately, there’s a simple solution: compare to the methodReturnType
s of known methods that return the required type. The final implementation uses a template class which implements a method for each required return type and copies the methodReturnType
s of these methods into an array. The template class pattern may be familiar if you’ve written NSProxy
s.
Free code!
Oolite’s code is complicated. The code given above is too simple. So here’s some code that’s just right.
The sample application uses a stripped-down version of the shader code from Oolite. It presents a scene consisting of a ball with per-pixel lighting and two moving lights, and controls to set two properties, colour and shininess (a combination of specular exponent and specular intensity). The window controller (cleverly named ExampleController
) moves the lights about on a timer.
The controls do not affect the scene directly; their values are stored in a model object (ExampleModel
) which knows nothing of OpenGL, shaders or lighting. The OpenGL view (ExampleOpenGLView
) sets up a shader material and binds the uniforms uColor
and uShininess
to the appropriate model properties:
_material = [[JAShaderMaterial alloc] initWithVertexShaderSource:vertSource fragmentShaderSource:fragSource]; // Bind shader uniforms to model attributes. [_material bindUniform:@"uColor" toObject:_model property:@selector(color)]; [_material bindUniform:@"uShininess" toObject:_model property:@selector(shininess)];
The material creates JAShaderUniform
objects to handle the individual uniforms. Now, whenever the shader is used (specifically, when its -apply
method is called), the uniform objects pull in their values from the model object. When the slider is moved, the model’s shininess
value is changed; when the scene next redraws, the shader’s uShininess
uniform reflects the new value. The controller and view don’t need to do anything to update them.
Limitations
The sample app, being a sample app, glosses over some issues. For one thing, binding targets are not retained, to avoid retain cycles. This is the Right Thing when the binding target is the owner of the material, but if it isn’t, you may inadvertently release an object which is bound to. Oolite avoids this by using a proxy-based weak reference system, which is beyond the scope of this article. The sample app also doesn’t bother to check whether its OpenGL context actually supports shaders.
However, the uniform implementation is pretty complete. It supports most of the types Oolite does, namely:
- Signed and unsigned
char
s,short
s andlong
s -
float
s anddouble
s - Vectors, in the form of
JAVector
structs. In real life, you’d want to use whatever vector struct or class you use for the rest of your application. -
NSPoint
s (asvec2
s) -
NSNumber
s (asfloat
s) -
NSColor
s (asvec4
s)
One Oolite feature removed for the example is filtering. Filters supported by Oolite include clamping numbers to the range 0..1, normalizing vectors, and converting quaternions to rotation matrices. These are simple operations that aren’t relevant to the focus of this article.
The sample code may be downloaded here (99 kB) and is distributed under the MIT/X11 License.
Disclaimer
Some of the above is not true. Some is misremembered. Some is glossed over and simplified. The code extracts are all simplified, untested code. Caveat lector.