Sunday, September 25, 2011

Use GL, Thats a Wrap!


Let GL Repeat Itself
As I'm starting to make improvements to Petunk, I've started to test the game play for maps larger than the screen bounds. And with larger maps means tracking the active ball on screen, and scrolling the scene to follow. The floor texture (green paper) would then need to be expanded past the screen bounds so that the camera seems to scroll along as new obstacles slide into view.
Often this style of scrolling ground texture I see done with tiles. And I like tiles too, but I did not want this green paper to looked tiled so I need another option. With Petunk I made the switch to using Cocos2d and for a lot of things its great. But earlier in my studies of GL I knew of a few texture settings that included texture pixel wrapping. The idea if your not familiar is that when a texture is used it can, clamp (stretch the last edge pixel), or repeat (loop back to the other side and continue mapping). I didn't find the settings for repeating textures in Cocos2d (if they are there) so I messed around with some classes until I could change the settings I was looking for.

But First Lets Run With Scissors
I needed to make a texture worth repeating, so your going to also get to learn a little Photoshop here.
I started with a rough edge piece of green craft paper (as per the style of Petunk) and want to get a 512x512 wrapping texture from it. The same could be done for many designs, and Photoshop make it easy to make sure your edge is ready to loop.
I duplicated the layer and over lapped the paper so it looked like paper on paper. Picked an ideal area of the file to be my target 512x512 by cropping it to that selection size. The crop alone will not produce a texture with a matching edge, but the file now sliced to 512x512 the Filter>Other>Offset.. tool becomes helpful. Using the offset dialog I set both a horizontal and vertical offset to give me enough room from the edge and selected to have it wrap around.
This will map the image to the offset and make the edges I didn't see before stand out so I can paint, blend, blur, and stamp until the seams go away. (Not the paper layer seam, that I want, but I did need to adjust it so it aligns back up with it self). The result is I get a wrappable texture, that GL can repeat on its triangles.

Thank You Open Source
With the repeatable texture we can now place it in the scene as a CCSprite and even butt a bunch up next to each other, but then thats just how tiling works. Instead we need to get access to the UVs of a sprite so we can modify them to go past 1 or more. The thing with UVs is each time the value is greater than a whole number it represents another wrap in the texture. Great for even smaller textures files on a large area of triangles. The get the effect of a sprite the wraps rather than moves I made a new class extension of a CCSprite.


#import "cocos2d.h"

@interface CCRepeatingSprite : CCSprite {

}

@end

No real meat here because this sets up my plan to override a few methods. When the sprite is created I need different texture settings established, I need a different triangle size than that of the image file it's going to reference, and I need it not to move in the scene.

#import "CCRepeatingSprite.h"

@implementation CCRepeatingSprite

+ (id)spriteWithFile:(NSString *)filename {
CCRepeatingSprite* sprite = [[[self alloc] initWithFile:filename] autorelease];
sprite.anchorPoint = CGPointZero;
ccTexParams params = { GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT };
[sprite.texture setTexParameters:&params];
return sprite;
}

Overriding the spriteWithFile gives me the first best opportunity to change the texture parameters which normally get set to clamp to edge. In this class method I've reset the parameters for this texture reference to instead repeat. Next it gets tricky because we need to mess with the 3D verticies directly.

- (void)setContentSize:(CGSize)size {

[super setContentSize:size];

quad_.bl.vertices = (ccVertex3F) { 0, 0, 0 };

quad_.br.vertices = (ccVertex3F) { size.width, 0, 0 };
quad_.tl.vertices = (ccVertex3F) { 0, size.height, 0 };
quad_.tr.vertices = (ccVertex3F) { size.width, size.height, 0 };
}

Along with other various important property settings the content size of a CCSprite defines where the vertices of the Gl triangle set gets positioned in the view. The verts are 3d points, but in most cases for cocos2d the 3rd value is always 0 so as to make it not so much 3rd dimensional. I've adjusted the points of this Square triangle set to position it's right and top bounds to the size I send in. (I intended this for background wrapping so I did not properly change the left and bottom points so it could optionally start somewhere other than the bottom left coordinate). In my case I set the aSprite.ContentSize = ccp(320,480) which means it will place the vertices at the 4 corners of the iPhone screen.

Playing with fire... I mean UVs


- (void)setPosition:(CGPoint)pos {

CCTexture2D *tex = (usesBatchNode_)?[textureAtlas_ texture]:texture_;

if(!tex) return;
    
    static float left = 0.f;
static float right = contentSize_.width / tex.pixelsWide;
static float top = contentSize_.height / tex.pixelsHigh;
static float bottom = 0.f;
CGPoint texOffset = ccp(pos.x / contentSize_.width, pos.y / contentSize_.height);
quad_.bl.texCoords.u = left + texOffset.x;
quad_.bl.texCoords.v = bottom + texOffset.y;
quad_.br.texCoords.u = right + texOffset.x;
quad_.br.texCoords.v = bottom + texOffset.y;
quad_.tl.texCoords.u = left + texOffset.x;
quad_.tl.texCoords.v = top + texOffset.y;
quad_.tr.texCoords.u = right + texOffset.x;
quad_.tr.texCoords.v = top + texOffset.y;
}

Now we go even deeper into the properties behind the 2d goodness that is cocos2d. We are going to change the UV values of each of the 4 vertices so they map to a different place in the texture file. If you don't already know, a texture file is mapped by a precent of its size in memory, which we can perceive as a 0 through 1 value (0% - 100%)
First thing here is I get a reference to the texture this instance is using because there is valuable data in the texture class. Then I set a few properties that define the percent of the texture is used within the content size so as not to get any stretching or shrinking from the texture when its drawn on to triangle smaller or bigger than it. These static left,right,top,bottom will be my baseline or origin of the textures UVs. Next I take in to account the value passed in by the position method and calculate and UV offset. Why a UV offset is because I going to move UV points and not the vertices position in this override. I get the difference in pixels converted to percentage so I can add it to my static baselines and change each pair of UVs of the 4 vert points in the CCSprite so the sprite is not moving, but where it draws from memory moves. Because we set the texture parameters to gl_repeat in the case the U or V value overtakes 100% or the whole number of 1, say 1.2 then the GL maps the memory back around to 20% or 0.2 of the texture file and no seam is ever seen due to our awesome Photoshoping work.


You Want Some More
This week Mystery Coconut made a great post about using core graphics to do parallax scrolling. And if your a Cocos2d fan you may already be using Cocos2d parallax layering class. Well this mod can fit right in because it's a CCSprite and it takes a position change. Lets say you have this neato background that you want to loop in the back, and a platform layer in the foreground. This sample works the same way, we take this tetris pattern that wraps well and save it as a 256x256. Using this CCRepeatingSprite class to load it. We can then set its content size to 480x320, and then layer it into the parallax class that Cocos2d provides or just take say half the value of the platform layer. m_repeatingBG.position = ccpMult(viewPoint, 0.5f);

Next thing we have is a background that will wrap with out needing to worry about repositioning a tile or repeating asset. 

No comments:

Post a Comment