{"id":3031,"date":"2014-08-01T16:30:12","date_gmt":"2014-08-01T15:30:12","guid":{"rendered":"http:\/\/t-machine.org\/?p=3031"},"modified":"2014-08-01T16:30:12","modified_gmt":"2014-08-01T15:30:12","slug":"opengl-es-2-video-on-iphone-in-3d-and-texture-map-sources","status":"publish","type":"post","link":"http:\/\/new.t-machine.org\/index.php\/2014\/08\/01\/opengl-es-2-video-on-iphone-in-3d-and-texture-map-sources\/","title":{"rendered":"OpenGL ES 2 &#8211; Video on iPhone in 3D, and Texture-Map sources"},"content":{"rendered":"<p><span style=\"color: red; font-size: larger\">This is a blog post I wrote 6 months ago, and my life got too busy to finish it off, finish testing the code, improving the text. So I&#8217;m publishing this now, warts and all. There are probably bugs. There are bits that I could explain better &#8211; but I don&#8217;t have time. Note that the code is ripped from apps where I ACTUALLY USED IT &#8211; so I know it does work, and works well!<\/span><\/p>\n<p>In brief:<\/p>\n<ol>\n<li>Using different &#8220;sources&#8221; of textures in OpenGL, not just images!\n<li>Using CALayer&#8217;s as a source &#8212; allows for video in 3D!\n<li>Using &#8220;Video straight from the video decoding chip&#8221; as a source &#8211; faster video!\n<\/ol>\n<p><!--more--><\/p>\n<h2>Procedural vs Mapped Textures<\/h2>\n<p>Now you&#8217;ve made both Procedural Textures (Part 5) and TextureMapped Textures (Part 6), it&#8217;s time to see what they can each do.<\/p>\n<h2>The Real World versus The Digital World<\/h2>\n<p>You can use a photo as a TextureMap, isn&#8217;t that great. Does it look like the thing you photo&#8217;d?<\/p>\n<p>Not much. In reality, the object should have hilights on sharp edges, and have shadows in the cracks. It probably refracts light (anything even partially transparent &#8211; including human skin). Some parts of it might be fractal &#8211; e.g. the bumpiness of waves in the sea remains the same even as you zoom out. Some parts would be static &#8211; e.g. in a brick wall: the arrangement of mortar between bricks doesn&#8217;t reappear at smaller scale as you zoom in!<\/p>\n<p>So &#8230; in an ideal world &#8230; we&#8217;d use Procedural textures for everything. We&#8217;d get realistic lighting, software control of every variable.<\/p>\n<p>But the world is full of video and photos already &#8211; billions of them &#8211; and none of them.<\/p>\n<p>So, to make something look realistic in 3D, you need to describe it completely in software.<\/p>\n<h2>Sources<\/h2>\n<p>Every texture in GL has a source.<\/p>\n<p>If it&#8217;s a texturemap, the simplest case: the &#8220;source&#8221; is a chunk of RAM on the GPU, labelled with an integer.<\/p>\n<p>If it&#8217;s a procedural texture, the &#8220;source&#8221; is the GPU-program (Vertex\/Fragment Shader pair) that generates colours on the fly. Since the GPU program could reference anything as input data, it may in turn have sources that feed into it. A common setup is:<\/p>\n<ul>\n<li>Procedural Texture: reptile-skin &#8230; MADE OF:\n<ul>\n<li>Texture-Map texture: &#8220;small scratches.png&#8221;\n<li>Procedural Texture: large hexagonal scales\/plates &#8230; MADE OF:\n<ul>\n<li>Procedural Texture: green-tint, with red shadows\n<li>Procedural Texture: lighting &#8230; MADE OF:\n<ul>\n<li>Texture-Map texture: light-map\n<li>Procedural Texture: dynamic white\/black lighting &#8230; MADE OF:\n<ul>\n<li>Texture-Map texture: Normal-map\n<li>Procedural Texture: lighting model (Fresnel)\n<\/ul>\n<\/ul>\n<\/ul>\n<\/ul>\n<\/ul>\n<p>In both cases, we can trivially swap the source for a different one, render our Draw call, and it will use the new texture instead.<\/p>\n<h2>Recap on Texturing<\/h2>\n<p>We know:<\/p>\n<ol>\n<li>TextureMaps are huge, slow to upload to GPU, and &#8220;dumb data&#8221;; if you want them to change, you have to re-upload the bytes. Your GPU RAM limits how complex they can get.\n<li>Procedural Textures are tiny, but have to be re-executed for every pixel you render; your GPU speed limits how complex they can get.\n<li>In GL ES 2, the two systems are partially merged\/integrated. TextureMaps need a simple Procedural texture (e.g. a one-line Fragment Shader, that passes-through the Texture Map values) to render them.\n<li>Neither system uses BufferObjects, even though BO\/VBO&#8217;s are designed for moving big chunks of data to\/from the GPU, largely because textures were &#8220;invented&#8221; \/ added to GL long before VBO&#8217;s existed\n<li>Shader Uniforms let us <a href=\"http:\/\/t-machine.org\/index.php\/2014\/02\/20\/opengl-es2-textures-2-of-3-gl-uniforms-with-animating-textures\/\">animate textures internally<\/a>\n<\/ol>\n<p>you understand the previous two tutorials (Part 5: Texturing (procedural), Part 6: TextureMapping (bitmaps)).  Now we can get onto the fun stuff&#8230;<\/p>\n<p>There&#8217;s almost an infinite amount of cool things you can do once you master GL&#8217;s texturing systems.<\/p>\n<h2>Animated textures: the magic of Uniforms<\/h2>\n<p>What comes next is a little odd for developers who are new to OpenGL: to efficiently animate something in OpenGL, you simply &#8220;render it differently each frame&#8221;. As I explained in Part 2, OpenGL is re-drawing everything, on every frame, already &#8211; so that this kind of &#8220;animation&#8221; comes for free.<\/p>\n<p>To make this work, we&#8217;ll either need to re-upload our data (the shader program itself, or the Attributes for each Draw call) every frame (with new values) &#8230; or: we need some kind of variable in the Shader that we can change from Frame to Frame.<\/p>\n<p>http:\/\/t-machine.org\/index.php\/2014\/01\/05\/glkit-extended-refactoring-the-view-controller\/<\/p>\n<p>Summary of previous two posts:<\/p>\n<ol>\n<li>Texturing in GL ES 2: REQUIRES an algorithm (executes on the GPU, as a ShaderProgram)\n<ul>\n<li>(see Part 5 for source code + explanation on doing this)\n<\/ul>\n<li>Texturing in GL ES 2: OPTIONALLY uses a bitmap (uploaded to the GPU&#8217;s RAM)\n<ul>\n<li>(see Part 6 for source code + explanation on doing this)\n<\/ul>\n<li>Apple provides a 1-line method to read &#8220;any PNG or JPG&#8221; and upload it to the GPU\n<li>OpenGL has a set of low-level methods to upload &#8220;raw bitmap data&#8221; to the GPU (we haven&#8217;t used those yet&#8230;)\n<\/ol>\n<p>There&#8217;s a huge world of options and features and effects you can do with OpenGL texturing. I&#8217;m trying to give you &#8220;just enough&#8221; to get you up-and-running on iOS, and then use Google (and OpenGL.org) to explore the other features yourself.<\/p>\n<p>But there&#8217;s a couple of important things that are specific to iOS, and you won&#8217;t find in most GL texts&#8230;<\/p>\n<h2>Embedding UIKit, CALayer and CGContext -&gt; OpenGL<\/h2>\n<p>Every windowing system needs a lowest common denominator\u00a0(LCD) where all the GUI code eventually gets converted into instructions for the GPU hardware. With Apple&#8217;s OS&#8217;s, that&#8217;s CALayer. UIKit has its own LCD (UIView) &#8211; but even UIView uses a CALayer internally.<\/p>\n<p>If we could integrate CALayer with OpenGL textures, we&#8217;d be able to convert <strong>any and all graphics on iOS<\/strong> into 3D. You could draw UITableView, or UILabel, inside an OpenGL view. You could take an UIImage and use it on the GPU (instead of &#8220;only images loaded from a PNG file&#8221;, as provided by Apple&#8217;s GLKit).<\/p>\n<blockquote><p>\nYou might have heard of CATransform3D, which seems to do the same thing. Under the hood, Apple uses OpenGL to implement CALayer. So, they&#8217;ve already done this integration &#8211; CALayer has a property &#8220;transform&#8221;. This is NOT the normal CGAffineTransform you see in UIKit &#8211; instead it&#8217;s a CATransform3D. That property is an OpenGL 4&#215;4 transform matrix, and you can use it to move and rotate CALayers &#8220;in 3D&#8221;.<\/p>\n<p>Unfortunately, CATransform3D is very limited (blocks you from using: Shaders, Textures, 3D Geometry), and it has some nasty bugs. Few developers use it, and it seems Apple has given up on it. My advice: don&#8217;t bother.\n<\/p><\/blockquote>\n<h3>UIKit\/CALayer&#8217;s rendering model<\/h3>\n<p>CALayer unifies all Apple rendering (both &#8220;to screen&#8221;, and &#8220;to CFImageRef \/ UIImage&#8221;) with a simple architecture:<\/p>\n<ol>\n<li>Everything, ultimately, renders to a CGContextRef.\n<li>When Apple is drawing to screen: Apple gives you a CGContextRef to use, and when you&#8217;re finished, they draw it to the screen\n<li>When you&#8217;re creating your own images programmatically: you create your own CGContextRef and convert it into an Image\n<li>All of Apple&#8217;s graphics classes (CALayer, UIView, UIKit, etc) have a method to draw themselves directly to a CGContextRef: &#8220;renderInContext:&#8221;\n<\/ol>\n<p>Externally, Apple provides special methods that let any developer &#8220;pull out&#8221; the results in a couple of formats:<\/p>\n<ol>\n<li>As a CFImageRef (almost identical to UIImage, but using Apple&#8217;s C-library)\n<li>(iOS only): as a new UIImage\n<li>As NSData (raw bytes, like a BMP or TIFF file)\n<\/ol>\n<p>I&#8217;ve seen people use the UIImage method above to do something like this (and, tragically, Apple&#8217;s engineering staff recommended it &#8211; but it&#8217;s a very stupid thing to do):<\/p>\n<ul>\n<li>(DONT DO THIS!)\n<ol>\n<li>Get a CGContextRef from Apple\n<li>Draw to it \/ use UIKit methods \/ whatever\n<li>Get a UIImage out of the CGContextRef\n<li>&#8230;\n<li>Use Apple&#8217;s UIImagePngRepresentation to convert the UIImage to a PNG file (in memory)\n<li>Use Apple&#8217;s GLKTextureLoader to load the in-memory file to the GPU as a texture\n<\/ol>\n<\/ul>\n<p>This <strong>works<\/strong> (and I&#8217;ve used it to debug my code in the past), but it&#8217;s insane. PNG is designed to be hugely CPU-intensive to compress, but fast to decompress. The net effect: this technique is often 50x slower than the correct approach &#8211; instead of &#8220;0.1 seconds&#8221;, it can easily take 5-10 seconds to send a single screen of data. If you&#8217;re trying to get realtime animation or rendering, that makes it impossible!<\/p>\n<p>Instead &#8230; both OpenGL and Apple support &#8220;raw stream of bytes, 4 numbers per pixel (Red, Green, Blue, Alpha)&#8221;. Apple&#8217;s preferred class for arrays-of-bytes is NSData.<\/p>\n<p>So, we&#8217;ll use the same mechanism, but upload straight to the GPU as a new texture. We only need a couple of static methods, and the output will be a GLK2Texture, so we store them in an Objective-C Category on GLK2Texture itself:<\/p>\n<p><em>GLK2Texture+CoreGraphics.h<\/em>:<br \/>\n[objc]<br \/>\n@interface GLK2Texture (CoreGraphics)<\/p>\n<p>+(CGContextRef) createCGContextForOpenGLTextureRGBAWidth:(int) width h:(int) height bitsPerPixel:(int) bpp shouldFlipY:(BOOL) flipY fillColorOrNil:(UIColor*) fillColor;<\/p>\n<p>+(GLK2Texture*) uploadTextureRGBAToOpenGLFromCGContext:(CGContextRef) context width:(int)w height:(int)h;<\/p>\n<p>@end<br \/>\n[\/objc]<\/p>\n<p><em>GLK2Texture+CoreGraphics.m<\/em>:<br \/>\n[objc]<br \/>\n@implementation GLK2Texture (CoreGraphics)<\/p>\n<p>+(CGContextRef) createCGContextForOpenGLTextureRGBAWidth:(int) width h:(int) height bitsPerPixel:(int) bpp shouldFlipY:(BOOL) flipY fillColorOrNil:(UIColor*) fillColor<br \/>\n{<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>You can create the CGContextRef yourself, manually, but it&#8217;s easy to get wrong. Apple&#8217;s library is a bit buggy on iOS (might get fixed in iOS 8?), and most of the options don&#8217;t work correctly <em>for this specific use-case<\/em> &#8211; so I recommend you use this method, which uses the &#8220;correct&#8221; format for the byte-array.<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n\tNSAssert( (width &amp; (width &#8211; 1)) == 0, @&quot;PowerVR will render your texture as ALL BLACK because you provided a width that&#8217;s not power-of-two&quot;);<br \/>\n\tNSAssert( (height &amp; (height &#8211; 1)) == 0, @&quot;PowerVR will render your texture as ALL BLACK because you provided a height that&#8217;s not power-of-two&quot;);<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p><strong>Important:<\/strong> if you try to capture a CGContext with width OR height that&#8217;s not &#8220;a power of two&#8221;, the GPU will silently reject it; debugging this is a nightmare. The GPU is capable of NPOT (non-power-of-two) textures &#8211; but requires a special image-format to make it work (&#8220;PVR&#8221;). So: we Assert here in case you ever send a NPOT CGContext by accident.<\/p>\n<p>[objc]<br \/>\n\t\/** Create a texture to render from *\/<br \/>\n\t\/*************** 1. Convert to NSData *\/<br \/>\n\tCGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();<br \/>\n\tCGContextRef context = CGBitmapContextCreate( NULL, width, height, 8, 4 * width, colorSpace, \/** NB: from Apple: incompatible types EXPECTED here according to API docs! *\/ (CGBitmapInfo) kCGImageAlphaPremultipliedLast  );<br \/>\n\tCGColorSpaceRelease( colorSpace );<br \/>\n[\/objc]<\/p>\n<p>&#8230;that&#8217;s standard code for creating a custom CGContextRef for use anywhere in their CA\/Quartz API. Check the CGBitmapContextCreate docs for more info (if interested).<\/p>\n<p>Note: the typecast looks dodgy (blame Apple, someone forgot to copy\/paste the enum values across) &#8211; but it&#8217;s &#8220;officially&#8221; correct. Also, Apple apparently hasn&#8217;t implemented the full API on iOS (it was originally OS X only) &#8211; so if you send one of the other enum values, Apple will either crash or silently fail. Finally &#8230; despite the name of this constant (kCGImageAlphaPremultipliedLast), Apple seems to send NON pre-multiplied alpha. Basically: this method sucks donkey, and hopefully someone at Apple will clean it up one day!<\/p>\n<p>[objc]<br \/>\n\tif( fillColor != nil )<br \/>\n\t{<br \/>\n\t\tCGContextSetFillColorWithColor(context, fillColor.CGColor);<br \/>\n\t\tCGContextFillRect(context, CGRectMake(0,0,width,height));<br \/>\n\t}<\/p>\n<p>\tif( flipY )<br \/>\n\t{<br \/>\n\t\tCGAffineTransform flipVertical = CGAffineTransformMake( 1, 0, 0, -1, 0, height );<br \/>\n\t\tCGContextConcatCTM(context, flipVertical);<br \/>\n\t}<\/p>\n<p>\treturn context;<br \/>\n}<br \/>\n[\/objc]<\/p>\n<p>&#8230;convenience code: if you&#8217;re generating a texture from a CALayer that comes from UIKit (i.e. from a UIView), Apple will automatically flip it upside-down. Which is tragic, since OpenGL and CALayer use the same idea of &#8220;up&#8221; &#8211; no flip was needed! But the support for &#8220;not flipping it&#8221; only exists on OS X, and Apple has deprecated it; in short: we do it ourselves.<\/p>\n<p>For CALayer&#8217;s that were created by CoreAnimation methods (not UIKit methods), the flipping is unnecessary.<\/p>\n<p>[objc]<br \/>\n+(GLK2Texture*) uploadTextureRGBAToOpenGLFromCGContext:(CGContextRef) context width:(int)w height:(int)h<br \/>\n{<br \/>\n\tvoid* resultAsVoidStar = CGBitmapContextGetData(context);<\/p>\n<p>\tsize_t dataSize = 4 * w * h; \/\/ RGBA = 4 * 8-bit components == 4 * 1 bytes<br \/>\n\tNSData* result = [NSData dataWithBytes:resultAsVoidStar length:dataSize];<\/p>\n<p>\tCGContextRelease(context);<br \/>\n[\/objc]<\/p>\n<p>&#8230;again, this is standard Apple code for &#8220;create an array of bytes, containing raw colours for each pixel, from the contents of a CGContextRef&#8221;. But the width, height, and number-of-bytes-per-pixel have to match with the numbers we used in the previous method.<\/p>\n<p>[objc]<br \/>\n\t\/*************** 2. Upload NSData to OpenGL *\/<br \/>\n\tGLK2Texture* newTextureReference = [[[GLK2Texture alloc] init] autorelease];<\/p>\n<p>[\/objc]<\/p>\n<p>Reminder: our GLK2Texture class automatically allocates a new texture on the GPU (as we did with VBOs, VAOs, etc), and saves the GPU name as GLK2Texture.glName<\/p>\n<p>[objc]<br \/>\n\tglBindTexture( GL_TEXTURE_2D, newTextureReference.glName);<br \/>\n[\/objc]<\/p>\n<p>&#8230;as with VBO\/VAO\/etc, we have to choose (&#8220;bind&#8221;) an active texture before configuring it&#8230;<\/p>\n<p>[objc]<br \/>\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);<br \/>\n[\/objc]<\/p>\n<p>&#8230;when the texture has too little detail (e.g. you zoomed-in too far), this controls how the GPU blurs\/interpolates\/smooths it. This is also how you&#8217;d enable and configure Mipmapping (but we&#8217;re ignoring that for now).<\/p>\n<p>[objc]<br \/>\n\tglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)w, (int)h, 0, GL_RGBA, GL_UNSIGNED_BYTE, [result bytes]);<\/p>\n<p>\treturn newTextureReference;<br \/>\n}<br \/>\n[\/objc]<\/p>\n<p>&#8230;finally, we upload the texture itself &#8211; and tell the GPU what width and height it is (these are saved on the GPU so it can render the texture later).<\/p>\n<h3>Demo: CGContext drawing on a 3D triangle<\/h3>\n<p>Assuming you&#8217;re using the refactored version of the sample code, we can now demo this very quickly. Make a GLK2DrawCallViewController subclass, and make a single Draw call:<\/p>\n<p><em>CALayerTextureViewController.m<\/em>:<br \/>\n[objc]<br \/>\n@implementation CALayerTextureViewController<\/p>\n<p>-(NSMutableArray*) createAllDrawCalls<br \/>\n{<br \/>\n\t\/** All the local setup for the ViewController *\/<br \/>\n\tNSMutableArray* result = [NSMutableArray array];<\/p>\n<p>\t\/** &#8212; Draw Call 1: triangle that contains a CALayer texture<br \/>\n\t *\/<\/p>\n<p>\tGLK2DrawCall* dcTri = [CommonGLEngineCode drawCallWithUnitTriangleAtOriginUsingShaders:<br \/>\n\t\t\t\t\t\t   [GLK2ShaderProgram shaderProgramFromVertexFilename:@&quot;VertexProjectedWithTexture&quot; fragmentFilename:@&quot;FragmentWithTexture&quot;]];<\/p>\n<p>\t\/** Do some drawing to a CGContextRef *\/<br \/>\n\tCGContextRef cgContext = [GLK2Texture createCGContextForOpenGLTextureRGBAWidth:256 h:256 bitsPerPixel:8 shouldFlipY:FALSE fillColorOrNil:[UIColor blueColor]];<br \/>\n\tCGContextSetFillColorWithColor( cgContext, [UIColor yellowColor].CGColor );<br \/>\n\tCGContextFillEllipseInRect( cgContext, CGRectMake( 50, 50, 150, 150 ));<\/p>\n<p>\t\/** Convert the CGContext into a GL texture *\/<br \/>\n\tGLK2Texture* newTexture = [GLK2Texture uploadTextureRGBAToOpenGLFromCGContext:cgContext width:256 height:256];<\/p>\n<p>\t\/** Add the GL texture to our Draw call \/ shader so it uses it *\/<br \/>\n\tGLK2Uniform* samplerTexture1 = [dcTri.shaderProgram uniformNamed:@&quot;s_texture1&quot;];<br \/>\n\t[dcTri setTexture:newTexture forSampler:samplerTexture1];<\/p>\n<p>\t\/** Set the projection matrix to Identity (i.e. &quot;dont change anything&quot;) *\/<br \/>\n\tGLK2Uniform* uniProjectionMatrix = [dcTri.shaderProgram uniformNamed:@&quot;projectionMatrix&quot;];<br \/>\n\tGLKMatrix4 rotatingProjectionMatrix = GLKMatrix4Identity;<br \/>\n\t[dcTri.shaderProgram setValueOutsideRenderLoopRestoringProgramAfterwards:&amp;rotatingProjectionMatrix forUniform:uniProjectionMatrix];<\/p>\n<p>\t[result addObject:dcTri];<\/p>\n<p>\treturn result;<br \/>\n}<br \/>\n@end<br \/>\n[\/objc]<\/p>\n<p>You should see the triangle with a yellow-circle on a blue-background:<\/p>\n<p>IIIIIIMMMAAAAAGGGGEEEEEEE<\/p>\n<p>It&#8217;s a very bad idea to overwrite a texture that&#8217;s already being rendered, but on alternate frames you could create a new GLK2Texture, upload a new snapshot of your CGContext, and then tell the drawcall to switch textures. This way you can easily render UIKit widgets directly into OpenGL &#8211; stick them on the walls of your 3D world.<\/p>\n<blockquote><p>\nNOTE: Apple&#8217;s UIScrollView has never quite worked properly, and any Apple widget &#8211; e.g. UITableView &#8211; that contains a UIScrollView inherits its problems. If\/when you use this approach to put UIKit widgets into 3D, you&#8217;ll get problems with touch-handling and scrolling. By default, Apple freezes OpenGL when a UITableView starts scrolling. This seems like a bug, but it&#8217;s a major performance optimization for UITableView. Plenty of Googling ahead of you if you want to go down that route&#8230;\n<\/p><\/blockquote>\n<h2>Embedding live video from the iPhone camera -&gt; OpenGL<\/h2>\n<p>Using the above trick, and the iOS video API&#8217;s, you could:<\/p>\n<ol>\n<li>Save video to disk\n<li>Load it from the file\n<li>Take each frame, and upload as a new GL texture\n<\/ol>\n<p>&#8230;but that won&#8217;t work for &#8220;live&#8221; video straight from the camera. You could get a bit cleverer, and use the low-level callbacks in AVFoundation to instead:<\/p>\n<ol>\n<li>Take any playing video\n<li>&#8220;Capture&#8221; individual frames while it decodes \/ streams\n<li>Draw the frame to a CGContextRef\n<li>Use the code from previous section to upload the CGContext&#8217;s contents to a GL texture\n<\/ol>\n<p>&#8230;and this works fine. For small videos, it&#8217;s pretty fast too. But isn&#8217;t this a bit ridiculous &#8211; all phones since the iPhone3GS contain dedicated &#8220;video chip&#8221; that handles the camera, and handles decoding MPG streams. If the data is already there, shouldn&#8217;t we be keeping it there, instead of downloading to CPU then uploading to GPU? Indeed!<\/p>\n<p>What we&#8217;ll do now is:<\/p>\n<ol>\n<li>Take any video-source from AVFoundation (live video from camera, video loaded from disk, etc)\n<li>Use a special Apple-provided Capture that keeps the data in raw video format on the device\n<li>Use a special Apple-provided method to re-route the frames directly to the GPU\n<li>Use a special Shader that renders the frames in the native video format (faster than converting to RGB)\n<\/ol>\n<p>&#8230;but it&#8217;s a lot of complex code. This took me a while to get right ;).<\/p>\n<h3>Use AVFoundation to capture (or decode) some video<\/h3>\n<p>AVF is awesome; if you don&#8217;t know how to use it yet, I recommend reading some tutorials and playing with it. It allows you to treat &#8220;any video, audio, or combination&#8221; as input, and do anything with it &#8211; save to disk, modify it on the fly, add graphical overlays, swap the sound-track, or even: upload it to OpenGL. And the code is identical, save for 1-3 lines, whether you&#8217;re taking video from the camera, or reading it from disk.<\/p>\n<blockquote><p>\nNOTE: make sure you add AVFoundation.framework to your project&#8217;s &#8220;Link Binary with Libraries&#8221; Build Phase; otherwise Xcode will rightly fail to build!\n<\/p><\/blockquote>\n<p>We&#8217;ll need a new ViewController, which supports the callback AVF uses when you&#8217;re capturing frames:<\/p>\n<p><em>VideoTextureViewController.h<\/em>:<br \/>\n[objc]<br \/>\n#import &lt;AVFoundation\/AVFoundation.h&gt;<\/p>\n<p>@interface VideoTextureViewController : GLK2DrawCallViewController &lt;AVCaptureVideoDataOutputSampleBufferDelegate&gt;<\/p>\n<p>@end<br \/>\n[\/objc]<\/p>\n<p>&#8230;and we have to do some fairly standard AVF setup. But first, to support the GL capture\/conversion, we have to use Apple&#8217;s CoreVideo, and pre-create a &#8220;texture cache&#8221; object:<\/p>\n<p><em>VideoTextureViewController.m<\/em>:<br \/>\n[objc]<br \/>\n@interface VideoTextureViewController ()<br \/>\n{<br \/>\n\t#pragma mark &#8211; Apple efficient video textures<br \/>\n\tCVOpenGLESTextureCacheRef coreVideoTextureCache;<br \/>\n}<br \/>\n&#8230;<br \/>\n&#8211; (AVCaptureSession*) setupAVCapture<br \/>\n{<br \/>\n    \/\/&#8211; Create CVOpenGLESTextureCacheRef for optimal CVImageBufferRef to GLES texture conversion.<br \/>\n#if COREVIDEO_USE_EAGLCONTEXT_CLASS_IN_API<br \/>\n    CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, self.localContext, NULL, &amp;coreVideoTextureCache);<br \/>\n#else<br \/>\n    CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, (__bridge void *)self.localContext, NULL, &amp;coreVideoTextureCache);<br \/>\n#endif<br \/>\n    if (err)<br \/>\n    {<br \/>\n        NSLog(@&quot;Error at CVOpenGLESTextureCacheCreate %d&quot;, err);<br \/>\n        return  nil;<br \/>\n    }<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Note that this CVOpenGLESTextureCacheCreate method from Apple requires our EAGLContext, as well as a pointer to our (currently empty) texture-cache. This allows Apple to do efficient conversion later on.<\/p>\n<p>Now we get on to typical AVF config: we create an AVSession and add an AVCaptureDevice to provide input (in this case: we&#8217;ll use the default Camera):<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n    AVCaptureSession* session;<br \/>\n    \/\/&#8211; Setup Capture Session<br \/>\n    session = [[[AVCaptureSession alloc] init] autorelease];<br \/>\n\t[session retain];<br \/>\n    [session beginConfiguration];<\/p>\n<p>    \/\/&#8211; Set preset session size.<br \/>\n    [session setSessionPreset:AVCaptureSessionPreset640x480];<\/p>\n<p>    \/\/&#8211; Creata a video device and input from that Device.  Add the input to the capture session.<br \/>\n    AVCaptureDevice * videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];<br \/>\n    if(videoDevice == nil)<br \/>\n        assert(0);<\/p>\n<p>    \/\/&#8211; Add the device to the session.<br \/>\n    NSError *error;<br \/>\n    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&amp;error];<br \/>\n    if(error)<br \/>\n        assert(0);<\/p>\n<p>    [session addInput:input];<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Then it gets interesting again. Instea of simply grabbing the output, we ask for the output <em>in the same format it starts in<\/em>, so that we spare the hardware from converting it back-and-forth (improving performance):<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n    \/\/&#8211; Create the output for the capture session.<br \/>\n\tAVCaptureVideoDataOutput * dataOutput = [[AVCaptureVideoDataOutput alloc] init];<br \/>\n\t[dataOutput setAlwaysDiscardsLateVideoFrames:YES]; \/\/ Probably want to set this to NO when recording<\/p>\n<p>    \/\/&#8211; Set to YUV420.<br \/>\n\t[dataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]<br \/>\n\t                                                         forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; \/\/ Necessary for manual preview<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>With the output created, we also use the AVF feature that gives us a callback each time a new video-frame is decoded, and point it back to the class we&#8217;re currently writing (then we add it to the AVF Session):<\/p>\n<p>[objc]<br \/>\n&#8230;.<br \/>\n    \/\/ Set dispatch to be on the main thread so OpenGL can do things with the data<br \/>\n\t[dataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];        <\/p>\n<p>    [session addOutput:dataOutput];<br \/>\n\t[session commitConfiguration];<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>And finally (as with normal AVF projects), we tell AVF to &#8220;start&#8221; the session (this will immediately start capturing video frames):<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n    [session startRunning];<\/p>\n<p>\treturn session;<br \/>\n}<br \/>\n[\/objc]<\/p>\n<h3>Use CoreVideo to convert AVFoundation video to OpenGL textures<\/h3>\n<p>This is where it gets tricky; the code that follows is correct &#8211; but Apple breaks some of OpenGL&#8217;s core concepts, and refuses to document how or why they did it. What we have here is a workaround based on &#8220;intelligent guesses&#8221;, &#8220;trial and error&#8221;, and &#8220;lots of experiments to reverse-engineer Apple&#8217;s code&#8221;.<\/p>\n<p>As a StackOverflow question from almost 2 years ago puts it:<\/p>\n<blockquote><p>\n&#8220;Where is the official documentation for CVOpenGLESTexture method types?&#8221; &#8230; &#8220;Unfortunately, there really isn&#8217;t any&#8221;\n<\/p><\/blockquote>\n<h4>Adam&#8217;s interpretation of CoreVideo, CVOpenGLESTexture, OpenGL ES upload on iOS<\/h4>\n<p>If Apple ever documents their API (!), I&#8217;ll come back and revise this. You don&#8217;t need to understand this to make it work &#8211; but if you want to tweak it or change it, it would help! So far as I can tell:<\/p>\n<ol>\n<li>When AVF has a frame in-memory, CoreVideo (CV) can access and act on that frame <em>without downloading it to the CPU<\/em>\n<li>Because the frame doesn&#8217;t exist on the CPU, Apple has to &#8220;fake&#8221; it to make it work with OpenGL (not Apple&#8217;s fault, it&#8217;s a side-effect of GL&#8217;s very old TextureMapping API)\n<ul>\n<li>(if my guess here is correct &#8230; perhaps a better solution from Apple would have been to extend OpenGL (as they&#8217;ve done before) and create OpenGL methods that supported this)\n<\/ul>\n<li>Apple has a class called a &#8220;texture cache&#8221; that <strong>does not necessarily cache textures<\/strong>; instead, it provides virtual textures backed by GPU memory (but not OpenGL memory)\n<li>Video-decoding happens on a different hardware chip to the one that does GL rendering; this means we have a multi-threading problem, by definition\n<li>The texture-cache (which isn&#8217;t a cache) automatically implements double-buffering internally to get around that. Depending on internal data, it changes the <em>name<\/em> of each GL texture from frame-to-frame.\n<li>ALSO: if the GPU blocks the Video Decoder at &#8220;the wrong moment&#8221;, it switches to triple-buffering, and then goes back to double-buffering\n<li>Net effect: over time, if the GPU is doing a lot of work, Apple&#8217;s code can end up generating infinitely-many virtual textures, instead of just 1 (or 2 for double-buffering)\n<\/ol>\n<p>Confused? No worries. Here&#8217;s some boilerplat code that does the job&#8230;<\/p>\n<h4>Convert a video-frame to OpenGL<\/h4>\n<p>We implement the callback method from AVF:<\/p>\n<p><em>VideoTextureViewController.m<\/em>:<br \/>\n[objc]<br \/>\n&#8211; (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection<br \/>\n{<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>First we grab some metadata about the frame &#8211; e.g. the width and height in pixels (could change from frame to frame if you switched camera in the middle of running the app):<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n    CVReturn err;<br \/>\n    CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);<br \/>\n    GLsizei width = (GLsizei)CVPixelBufferGetWidth(pixelBuffer);<br \/>\n    GLsizei height = (GLsizei)CVPixelBufferGetHeight(pixelBuffer);<br \/>\n\tif (!coreVideoTextureCache)<br \/>\n    {<br \/>\n        NSLog(@&quot;No video texture cache&quot;);<br \/>\n        return;<br \/>\n    }<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Remember: (my guess) the coreVideoTextureCache isn&#8217;t really a cache, it&#8217;s holding &#8220;virtual&#8221; textures. We can safely &#8220;release&#8221; them, and it will have no effect &#8211; they weren&#8217;t real to start with.<\/p>\n<p>Also, this coreVideoTextureCache seems to &#8220;ignore&#8221; successive frames unless you explicitly call the &#8220;Flush&#8221; method; Apple&#8217;s header files seem to say it&#8217;s not necessary, that flushing is automatic &#8211; but this is not true (try commenting it out).<\/p>\n<p>We&#8217;re CFRelease&#8217;ing because we&#8217;re about to use a C method with the word &#8220;Create&#8221; in it, that means &#8220;automatically retain it&#8221;.<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n\t\tif( appleCreatedTexture1 != NULL )<br \/>\n\t\tCFRelease( appleCreatedTexture1 ); \/\/ Apple requires this<br \/>\n\t\tif( appleCreatedTexture2 != NULL )<br \/>\n\t\tCFRelease( appleCreatedTexture2 ); \/\/ Apple requires this<\/p>\n<p>\t\t\/\/ Periodic texture cache flush every frame<br \/>\n\t\tCVOpenGLESTextureCacheFlush(coreVideoTextureCache, 0); \/\/ Apple requires this<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Now we &#8220;create&#8221; new virtual-textures from the cache. We need two textures for one frame, because the default encoding for video is a pair of images (Y and UV).<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n \/\/ Y-plane<br \/>\n    err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,<br \/>\n                                                       coreVideoTextureCache,<br \/>\n                                                       pixelBuffer,<br \/>\n                                                       NULL,<br \/>\n                                                       GL_TEXTURE_2D,<br \/>\n                                                       GL_RED_EXT,<br \/>\n                                                       width,<br \/>\n                                                       height,<br \/>\n                                                       GL_RED_EXT,<br \/>\n                                                       GL_UNSIGNED_BYTE,<br \/>\n                                                       0,<br \/>\n                                                       &amp;appleCreatedTexture1);<\/p>\n<p>    if (err)<br \/>\n    {<br \/>\n        NSLog(@&quot;Error at CVOpenGLESTextureCacheCreateTextureFromImage %d&quot;, err);<br \/>\n    }   <\/p>\n<p>    \/\/ UV-plane<br \/>\n    err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,<br \/>\n                                                       coreVideoTextureCache,<br \/>\n                                                       pixelBuffer,<br \/>\n                                                       NULL,<br \/>\n                                                       GL_TEXTURE_2D,<br \/>\n                                                       GL_RG_EXT,<br \/>\n                                                       width\/2,<br \/>\n                                                       height\/2,<br \/>\n                                                       GL_RG_EXT,<br \/>\n                                                       GL_UNSIGNED_BYTE,<br \/>\n                                                       1,<br \/>\n                                                       &amp;appleCreatedTexture2);<\/p>\n<p>    if (err)<br \/>\n    {<br \/>\n        NSLog(@&quot;Error at CVOpenGLESTextureCacheCreateTextureFromImage %d&quot;, err);<br \/>\n    }<br \/>\n[\/objc]<\/p>\n<p>At this point, we have:<\/p>\n<ol>\n<li>A virtual texture with Y \/ luminance\n<li>A virtual texture with UV \/ Chroma\n<li>Both textures have been &#8220;forced&#8221; to update themselves with the latest video-frame (thanks to the &#8230;Flush&#8230; command)\n<\/ol>\n<p>We want to create simple, standard GL texture references out of these virtual textures. We&#8217;ve got a nice, simple class for that &#8211; GLK2Texture.<\/p>\n<p>But it&#8217;ll need extending to support &#8220;virtual&#8221; textures:<\/p>\n<ol>\n<li>The textures are created by CoreVideo, and already have a glName; we need a new constructor to GLK2Texture\n<li>The virtual textures must NOT be deleted from GPU when the GLK2Texture deallocs (this corrupts the rendering state); we need a way to turn-off that behaviour\n<li>When Apple changes the virtual texture to a different glName, we need to switch our glName too without deleting\/reallocing our GLK2Texture; it&#8217;s the same texture, but its GPU name has changed\n<\/ol>\n<p>These are odd (or dangerous) changes, specific to CoreVideo, so we put them into a category on GLK2Texture:<\/p>\n<p><em>GLK2Texture+CoreVideo.h<\/em>:<br \/>\n[objc]<br \/>\n@interface GLK2Texture (CoreVideo)<\/p>\n<p>+(GLK2Texture*) texturePreCreatedByApplesCoreVideo:(CVOpenGLESTextureRef) appleCoreVideoTexture<\/p>\n<p>-(void) liveAlterGLNameToWorkaroundAppleCoreVideoBug:(GLuint) newName<\/p>\n<p>@end<br \/>\n[\/objc]<\/p>\n<p>&#8230;with an ordinary constructor, reading the virtual-texture&#8217;s name at runtime:<\/p>\n<p><em>GLK2Texture+CoreVideo.m<\/em>:<br \/>\n[objc]<br \/>\n@implementation GLK2Texture (CoreVideo)<\/p>\n<p>+(GLK2Texture*) texturePreCreatedByApplesCoreVideo:(CVOpenGLESTextureRef) appleCoreVideoTexture<br \/>\n{<br \/>\n\tGLK2Texture* newValue = [[[GLK2Texture alloc] initWithName:CVOpenGLESTextureGetName(appleCoreVideoTexture)]autorelease];<\/p>\n<p>\treturn newValue;<br \/>\n}<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Generally, you want GL names to be constant. We can&#8217;t do that here, but I don&#8217;t want to expose a dangerous feature for normal usage. To be on the safe side, we use an ObjC class-extension-header. This makes it &#8220;private, but visible internally to the library&#8221;:<\/p>\n<p><em>GLK2Texture_MutableName.h<\/em>:<br \/>\n[objc]<br \/>\n@interface GLK2Texture()<br \/>\n@property(nonatomic, readwrite) GLuint glName;<br \/>\n@end<br \/>\n[\/objc]<\/p>\n<p><em>GLK2Texture.h<\/em>:<br \/>\n[objc]<br \/>\n&#8230;<br \/>\n#import &quot;GLK2Texture_MutableName.h&quot;<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>(any class that imports that header gets to treat GLK2Texture.glName as a writeable property. Classes that &#8220;only&#8221; import GLK2Texture.h see it as a readonly property)<\/p>\n<p>GLK2Texture+CoreVideo imports that header, and is able to implement a method that changes this property:<\/p>\n<p><em>GLK2Texture+CoreVideo.m<\/em>:<br \/>\n[objc]<br \/>\n&#8230;<br \/>\n-(void) liveAlterGLNameToWorkaroundAppleCoreVideoBug:(GLuint) newName<br \/>\n{<br \/>\n\tself.glName = newName;<br \/>\n}<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>Putting this all together, we&#8217;re able to selectively create (or update) our GLK2Texture when the Apple texture changes. If we&#8217;re creating, we also add the GLK2Texture to our Drawcall &#8211; but if we&#8217;re simply overwriting the .glName, we don&#8217;t need to (the render loop will re-read the new value next time it loops round. Isn&#8217;t OOP wonderful? ;)) :<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n\tBOOL glNameWasCreatedOrChanged = FALSE;<br \/>\n\tif( self.textureVideoLuminance == nil )<br \/>\n\t{<br \/>\n\t\tglNameWasCreatedOrChanged = TRUE;<\/p>\n<p>\t\tself.textureVideoLuminance = [GLK2Texture texturePreCreatedByApplesCoreVideo:appleCreatedTexture1];<br \/>\n\t\tself.textureVideoLuminance.willDeleteOnDealloc = FALSE;<\/p>\n<p>\t\t[self.drawCallThatRendersVideoTextures setTexture:self.textureVideoLuminance forSampler:[self.drawCallThatRendersVideoTextures.shaderProgram uniformNamed:@&quot;s_texture1&quot;]];<br \/>\n\t}<br \/>\n\telse if( self.textureVideoLuminance.glName != CVOpenGLESTextureGetName(appleCreatedTexture1) )<br \/>\n\t{<br \/>\n\t\tglNameWasCreatedOrChanged = TRUE;<\/p>\n<p>\t\t[self.textureVideoLuminance liveAlterGLNameToWorkaroundAppleCoreVideoBug:CVOpenGLESTextureGetName(appleCreatedTexture1)];<br \/>\n\t}<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>But what&#8217;s this &#8220;glNameWasCreatedOrChanged&#8221; BOOL for?<\/p>\n<p>Well, it turns out that Apple&#8217;s virtual textures are more than a little bizarre. By default, the render solid black &#8211; unless you tell them to &#8220;clamp texturemap to edge&#8221;. This is very strange, and even more strange: it needs re-doing every time Apple flips their double-buffer. None of this is documented, Apple&#8217;s source code does it every frame without explanation. But if you turn it off (it&#8217;s seemingly &#8220;useless&#8221; code), everything breaks.<\/p>\n<p>Anyway, when it&#8217;s changed, we re-configure the GL virtual texture with clamping:<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n\tif( glNameWasCreatedOrChanged )<br \/>\n\t{<br \/>\n\t\tglActiveTexture(GL_TEXTURE0);<br \/>\n\t\tglBindTexture( CVOpenGLESTextureGetTarget(appleCreatedTexture1), CVOpenGLESTextureGetName(appleCreatedTexture1));<br \/>\n\t\tglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);<br \/>\n\t\tglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);<br \/>\n\t}<br \/>\n&#8230;<br \/>\n[\/objc]<\/p>\n<p>&#8230;then we repeat all that for the other texture (UV \/ Chroma). C.f. the GitHub source to see (it&#8217;s a copy\/paste, with the references changed).<\/p>\n<h4>Custom Shader to render Y\/UV video colours as RGB<\/h4>\n<p>OpenGL ES uses RGB \/ RGBA &#8211; it doesn&#8217;t support YUV and other exotic formats.<\/p>\n<p>Apple <a href=\"https:\/\/developer.apple.com\/library\/ios\/samplecode\/GLCameraRipple\/Listings\/GLCameraRipple_Shaders_Shader_fsh.html#\/\/apple_ref\/doc\/uid\/DTS40011222-GLCameraRipple_Shaders_Shader_fsh-DontLinkElementID_9\">provides us the Fragment shader<\/a> that will do the conversion on-the-fly:<\/p>\n<p><em>FragmentVideoPairTexture.fsh<\/em>:<br \/>\n[c]<br \/>\nvarying mediump vec2 varyingtextureCoordinate;<\/p>\n<p>uniform sampler2D s_texture1, s_texture2;<\/p>\n<p>void main()<br \/>\n{<br \/>\n\tmediump vec3 yuv;<br \/>\n\tlowp vec3 rgb;<\/p>\n<p>\tyuv.x = texture2D(s_texture1, varyingtextureCoordinate).r;<br \/>\n\tyuv.yz = texture2D(s_texture2, varyingtextureCoordinate).rg &#8211; vec2(0.5, 0.5);<\/p>\n<p>    \/\/ Using BT.709 which is the standard for HDTV<br \/>\n    rgb = mat3(      1,       1,      1,<br \/>\n\t\t\t   0, -.18732, 1.8556,<br \/>\n\t           1.57481, -.46813,      0) * yuv;<\/p>\n<p>    gl_FragColor = vec4(rgb, 1);<br \/>\n}<br \/>\n[\/c]<\/p>\n<h4>Putting it all together: live video texturing a rotating cube<\/h4>\n<p>Finally, we can write the Draw calls that use all the above:<\/p>\n<p><em>VideoTextureViewController.m<\/em>:<br \/>\n[objc]<br \/>\n&#8230;<br \/>\n-(NSMutableArray*) createAllDrawCalls<br \/>\n{<br \/>\n\tGLK2DrawCall* dcCube = [CommonGLEngineCode drawCallWithUnitCubeAtOriginUsingShaders:<br \/>\n\t[GLK2ShaderProgram shaderProgramFromVertexFilename:@&quot;VertexProjectedWithTexture&quot; fragmentFilename:@&quot;FragmentVideoPairTexture&quot;]];<\/p>\n<p>\t[result addObject:dcCube];<\/p>\n<p>\t[self setupAVCapture];<\/p>\n<p>\treturn result;<br \/>\n}<br \/>\n[\/objc]<\/p>\n<p>&#8230;and we can implement a Uniform that changes the projection every frame, causing the cube to rotate:<\/p>\n<p>[objc]<br \/>\n&#8230;<br \/>\n-(void)willRenderDrawCallUsingVAOShaderProgramAndDefaultUniforms:(GLK2DrawCall *)drawCall<br \/>\n{<br \/>\n\t\/*************** Rotate the entire world, for Shaders that support it *******************\/<br \/>\n\tGLK2Uniform* uniProjectionMatrix = [drawCall.shaderProgram uniformNamed:@&quot;projectionMatrix&quot;];<br \/>\n\tif( uniProjectionMatrix != nil )<br \/>\n\t{<br \/>\n\t\t\/** Generate a smoothly increasing value using GLKit&#8217;s built-in frame-count and frame-timers *\/<br \/>\n\t\tlong slowdownFactor = 5; \/\/ scales the counter down before we modulus, so rotation is slower<br \/>\n\t\tlong framesOutOfFramesPerSecond = self.framesDisplayed % (self.framesPerSecond * slowdownFactor);<br \/>\n\t\tfloat radians = framesOutOfFramesPerSecond \/ (float) (self.framesPerSecond * slowdownFactor);<\/p>\n<p>\t\t\/\/ rotate it<br \/>\n\t\tGLKMatrix4 rotatingProjectionMatrix = GLKMatrix4MakeRotation( radians * 2.0 * M_PI, 1.0, 1.0, 1.0 );<\/p>\n<p>\t\t[drawCall.shaderProgram setValue:&amp;rotatingProjectionMatrix forUniform:uniProjectionMatrix];<br \/>\n\t}<br \/>\n}<br \/>\n[\/objc]<\/p>\n<p>Run it on the device, and you&#8217;ll get a rotating cube:<\/p>\n<blockquote><p>\nNOTE: this will crash if you run it on Simulator &#8211; there&#8217;s no camera on the Simulator!\n<\/p><\/blockquote>\n<p>IMG_4938.PNG IMG_4938.PNG IMG_4938.PNG<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is a blog post I wrote 6 months ago, and my life got too busy to finish it off, finish testing the code, improving the text. So I&#8217;m publishing this now, warts and all. There are probably bugs. There are bits that I could explain better &#8211; but I don&#8217;t have time. Note that [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[51,20],"tags":[],"_links":{"self":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3031"}],"collection":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/comments?post=3031"}],"version-history":[{"count":10,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3031\/revisions"}],"predecessor-version":[{"id":3301,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3031\/revisions\/3301"}],"wp:attachment":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/media?parent=3031"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/categories?post=3031"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/tags?post=3031"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}