Monday, July 30, 2012

Zissou2d Part 3: The Sprite (Textures and Programs)










As I mentioned in my previous post, the Sprite is where all the action takes place, and by that, I mean the drawing. In CC2d, they've made it so that each object that has to draw itself can chose from a set of shading programs pre-loaded in the ShaderCache. The choice is made depending on what the Sprite has to draw.

Usually, a Sprite will be loaded with a texture image, which means that you will need a shader that can sample those textures. If you have no idea what I'm talking about, you can refer to my previous blog post about texturing and compression. On the other hand, you could have a Sprite that only contains a primitive shape, like a triangle, and no texture. For this, you obviously wouldn't need a program that can sample textures, thus that program would be simpler.

In this post, I will show you how to build a basic ShaderCache, as well as a TextureCache (insures an image, or texture, is loaded only once and can be re-used for different Sprites). Our ShaderCache will only contain one program for now; one that can draw textured objects (because I've already covered that in the previous post I mentioned, and that I've already written the shading program to do just that).

Also, from now on I'll have a Git of the project on GitHub.


First, let me show you a little diagram that summarizes what I just said:


Note: When I drew the diagram, I thought that I would have to implement a Texture2d object that wraps a texture image, just like in CC2d. It turns out that if you use GLKTextureLoader to load an image, a GLKTextureInfo instance is created and you can pass it around to objects that want to use that texture. That thing contains all the information you need to make calls to OpenGL.

ShaderCache and GLProgram

The first thing I'm going to do is implement the ShaderCache and the Program because it's going to require a bit more code (even though I'll be copy-pasting some stuff I already wrote). I'm also going to leave the Texture and TextureCache for last because I will be using the GLKTextureLoader which will make our job quite easy.

Here's the code for the program (this is almost exactly like what I posted here. The only difference being that it's now a normal class with instance methods instead of a helper class with only class methods):

/////////////////////////////////////////////////////////////////////////////////////////////////////
// Interface (.h)
@interface GLProgram : NSObject
- (void)use;
- (id)initWithVertexShader:(NSString*)vertexShaderName fragmentShader:(NSString*)fragmentShaderName;
@end
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation (.m)
@interface GLProgram(){
GLuint _program;
}
@end
@implementation GLProgram
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType
{
// Load the shader in memory
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:shaderName ofType:@"glsl"];
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if(!shaderString)
{
NSLog(@"Error loading shader: %@", error.localizedDescription);
exit(1);
}
// Create the shader inside openGL
GLuint shaderHandle = glCreateShader(shaderType);
// Give that shader the source code loaded in memory
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
// Compile the source code
glCompileShader(shaderHandle);
// Get the error messages in case the compiling has failed
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLint logLength;
glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0)
{
GLchar *log = (GLchar *)malloc(logLength);
glGetShaderInfoLog(shaderHandle, logLength, &logLength, log);
NSLog(@"Shader compile log:\n%s", log);
free(log);
}
exit(1);
}
return shaderHandle;
}
- (id)initWithVertexShader:(NSString*)vertexShaderName fragmentShader:(NSString*)fragmentShaderName
{
self = [super init];
if (self) {
// Compile both shaders
GLuint vertexShader = [self compileShader:vertexShaderName withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:fragmentShaderName withType:GL_FRAGMENT_SHADER];
// Create the program in openGL, attach the shaders and link them
GLuint programHandle = glCreateProgram();
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
glLinkProgram(programHandle);
// Get the error message in case the linking has failed
GLint linkSuccess;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE)
{
GLint logLength;
glGetProgramiv(programHandle, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0)
{
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(programHandle, logLength, &logLength, log);
NSLog(@"Program link log:\n%s", log);
free(log);
}
exit(1);
}
_program = programHandle;
}
return self;
}
- (void)use
{
glUseProgram(_program);
}
@end
view raw GLProgram.m hosted with ❤ by GitHub

As you can see, it's quite simple. The class has an initializer which takes the .glsl file name for both the vertex and the fragment shader. That initializer then takes care of compiling each file, and linking them inside a newly created OpenGL program. It also logs any errors in the process. The use method is going to be used later, when we want to tell GL which program it should use.

And now here's the code for the ProgramManager:

/////////////////////////////////////////////////////////////////////////////////////////////////////
// Interface (.h)
#import "GLProgram.h"
@interface ProgramManager : NSObject
+ (ProgramManager*)sharedProgramManager;
- (GLProgram*)getDefaultProgram;
@end
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation (.m)
#import "ProgramManager.h"
#define VERTEX_SHADER @"vertex"
#define FRAGMENT_SHADER @"fragment"
static ProgramManager* _sharedProgramManager = nil;
@interface ProgramManager()
@property (strong, nonatomic) GLProgram *defaultProgram;
@end
@implementation ProgramManager
@synthesize defaultProgram = _defaultProgram;
+ (ProgramManager*)sharedProgramManager
{
if (!_sharedProgramManager)
_sharedProgramManager = [[ProgramManager alloc] init];
return _sharedProgramManager;
}
- (GLProgram*)getDefaultProgram
{
if (self.defaultProgram) {
return self.defaultProgram;
}
return nil;
}
- (void)loadDefaultProgram
{
self.defaultProgram = [[GLProgram alloc] initWithVertexShader:VERTEX_SHADER fragmentShader:FRAGMENT_SHADER];
}
-(id) init
{
self = [super init];
if(self)
{
[self loadDefaultProgram];
}
return self;
}
@end


Key points:
  • You can access it from anywhere
  • The first time it is accessed through the sharedProgramManager, it will be instantiated and the default program will be created
  • There's only a single program in it right now but you could have an array of programs instead

How do we use this thing, and where?

Well, when you initialize your sprite, you will use the following call to give that sprite the default program (The Sprite object needs to have a program property):

self.program = [[ProgramManager sharedProgramManager] getDefaultProgram];

TextureCache

The TextureCache is fairly simple:

  • You add images to it by passing a file name
  • The object checks if it already exists
  • If it doesn't exist, it will load the texture using GLKTextureLoader and add it to it's cache
  • The object return a GLKTextureInfo object containing all the information to use that texture

The code:


/////////////////////////////////////////////////////////////////////////////////////////////////////
// Interface (.h)
#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>
@interface TextureCache : NSObject
+ (TextureCache*) sharedTextureCache;
- (GLKTextureInfo*)addImage:(NSString*)fileName;
@end
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation (.m)
#import "TextureCache.h"
static TextureCache *_sharedTextureCache;
@interface TextureCache()
@property (strong, nonatomic) NSMutableDictionary *textures;
@end
@implementation TextureCache
@synthesize textures = _textures;
+ (TextureCache*)sharedTextureCache
{
if (!_sharedTextureCache)
_sharedTextureCache = [[self alloc] init];
return _sharedTextureCache;
}
- (id)init
{
self=[super init];
if(self) {
self.textures = [NSMutableDictionary dictionaryWithCapacity: 10];
}
return self;
}
- (GLKTextureInfo*)addImage:(NSString *)fileName
{
GLKTextureInfo *textureInfo = [self.textures objectForKey:fileName];
if(!textureInfo)
{
NSError *error;
NSString* filePath;
filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"png"];
textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:&error];
if(error) {
NSLog(@"Error loading texture from image: %@", error);
}
}
return textureInfo;
}
@end
view raw TextureCache.m hosted with ❤ by GitHub


Key points:

  • This one is also accessible from anywhere
  • GLKTextureLoader returns a GLKTextureInfo instance when a texture is created. This is what we will pass to the Sprite (In CC2d, there is a Texture2d object being passed around, just like in the diagram I made)
  • The keys in the dictionary are the file names, so you can't load two textures with the same name

Back to the Sprite


With all of the work we've done, we have to change the interface of the Sprite object to this:

#import <GLKit/GLKit.h>
#import "Entity.h"
#import "TextureCache.h"
#import "GLProgram.h"
#import "ProgramManager.h"
@interface Sprite : Entity
@property (strong, nonatomic) GLKTextureInfo *textureInfo;
@property (strong, nonatomic) GLProgram *program;
+ (id)spriteWithFile:(NSString*)filename;
@end

The interface contains the three following elements:
  1. Texture property
  2. Program property
  3. Class method that returns a Sprite initialized with the given image
And now the implementation:

#import "Sprite.h"
@implementation Sprite
@synthesize textureInfo = _textureInfo;
@synthesize program = _program;
- (void)draw
{
NSLog(@"Drawing a sprite");
}
- (id)initWithFile:(NSString*)fileName
{
self = [super init];
if(self){
self.textureInfo = [[TextureCache sharedTextureCache] addImage:fileName];
self.program = [[ProgramManager sharedProgramManager] getDefaultProgram];
}
return self;
}
+ (id)spriteWithFile:(NSString*)filename
{
return [[self alloc] initWithFile:filename];
}
@end
view raw sprite.m hosted with ❤ by GitHub


Summary


Here's a summary of what we've done:
  • ProgramManager
    • Responsible for creating and holding the default programs
    • Other objects that need to draw something can ask for one of those programs
  • GLProgram
    • Loads the .GLSL files, compiles and links the vertex and fragment shader
  • TextureCache
    • Contains information (GLKTextureInfo) about all the textures that have been loaded
    • Insures an image can only be loaded once
  • Sprite
    • Added a class method that initializes a Sprite with a given image
    • This image is added to the TextureCache which returns a GLKTextureInfo
    • Now has both a texture and a program to work with


What now?


Well, we have to use that texture and the program to draw the texture on screen, but that will be the subject of another post!

Link to the project so far

1 comment:

Bob S said...

What is the demo supposed to do? All I get is a pink screen in the iOS simulator?