Our final enhancement to ModelViewer wraps a
simple grid texture around each of the surfaces in the parametric gallery,
as shown in Figure 1. We need to store
only one cell of the grid in an image file; OpenGL can repeat the source
pattern as many times as desired.
The image file can be in a number of different
file formats, but we’ll go with PNG for now. It’s popular because it
supports an optional alpha channel, lossless compression, and variable
color precision.
To keep things simple, the
first step is rewinding a bit and using parametric surfaces exclusively.
Simply revert the ApplicationEngine::Initialize method
so that it uses the sphere and cone shapes. To do this, find the following
code:
string path = m_resourceManager->GetResourcePath();
surfaces[0] = new ObjSurface(path + "/micronapalmv2.obj");
surfaces[1] = new ObjSurface(path + "/Ninja.obj");
And replace it with the following:
surfaces[0] = new Cone(3, 1);
surfaces[1] = new Sphere(1.4f);
Keep everything else the same. We’ll enhance
the IResourceManager interface later to support image
loading.
Next you need to add the actual image file to
your Xcode project. Xcode
automatically deploys these resources to your iPhone. Even though this
example has only a single image file, I recommend creating a dedicated
group anyway. Right-click the ModelViewer root in the
OverviewAdd→New Group, and call it
Textures. Right-click the new group, and choose Get
Info. To the right of the Path label on the General
tab, click Choose, and create a new folder called
Textures. Click Choose, and close the group info
window. pane, choose
Right-click the new group, and choose
Add→Existing Files. Select the PNG file,
click Add, and in the next dialog box, make sure the “Copy items” checkbox
is checked; then click Add.
1. Enhancing IResourceManager
Unlike OBJ files, it’s a bit nontrivial to
decode the PNG file format by hand since it uses a lossless compression
algorithm. Rather than manually decoding the PNG file in the application
code, it makes more sense to leverage the infrastructure that Apple
provides for reading image files. Recall that the
ResourceManager implementation is a mix of
Objective-C and C++; so, it’s an ideal place for calling Apple-specific
APIs. Let’s make the resource manager responsible for decoding the PNG
file. The first step is to open Interfaces.hpp and
make the changes shown in Example 1.
New lines are in bold (the changes to the last two lines, which you’ll
find at the end of the file, are needed to support a change we’ll be
making to the rendering engine).
Example 1. Enhanced IResourceManager
struct IResourceManager { virtual string GetResourcePath() const = 0; virtual void LoadPngImage(const string& filename) = 0; virtual void* GetImageData() = 0; virtual ivec2 GetImageSize() = 0; virtual void UnloadImage() = 0; virtual ~IResourceManager() {} };
// ... namespace ES1 { IRenderingEngine* CreateRenderingEngine(IResourceManager* resourceManager); } namespace ES2 { IRenderingEngine* CreateRenderingEngine(IResourceManager* resourceManager); }
|
Now let’s open
ResourceManager.mm and update the actual
implementation class (don’t delete the #imports, the
using statement, or the
CreateResourceManager definition). It needs two
additional items to maintain state: the decoded memory buffer
(m_imageData) and the size of the image
(m_imageSize). Apple provides several ways of loading
PNG files; Example 2 is the simplest way of
doing this.
Example 2. ResourceManager with PNG loading
class ResourceManager : public IResourceManager { public: string GetResourcePath() const { NSString* bundlePath =[[NSBundle mainBundle] resourcePath]; return [bundlePath UTF8String]; } void LoadPngImage(const string& name) { NSString* basePath = [NSString stringWithUTF8String:name.c_str()]; NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; NSString* fullPath = [resourcePath stringByAppendingPathComponent:basePath]; UIImage* uiImage = [UIImage imageWithContentsOfFile:fullPath]; CGImageRef cgImage = uiImage.CGImage; m_imageSize.x = CGImageGetWidth(cgImage); m_imageSize.y = CGImageGetHeight(cgImage); m_imageData = CGDataProviderCopyData(CGImageGetDataProvider(cgImage)); } void* GetImageData() { return (void*) CFDataGetBytePtr(m_imageData); } ivec2 GetImageSize() { return m_imageSize; } void UnloadImage() { CFRelease(m_imageData); } private: CFDataRef m_imageData; ivec2 m_imageSize; };
|
Most of Example 2
is straightforward, but LoadPngImage deserves some
extra explanation:
Next we need to make sure that the resource
manager can be accessed from the right places. It’s already instanced in
the GLView class and passed to the application
engine; now we need to pass it to the rendering engine as well. The
OpenGL code needs it to retrieve the raw image data.Go ahead and change
GLView.mm so that the resource manager gets passed to
the rendering engine during construction. Example 3 shows the relevant section of code
(additions are shown in bold).
Example 3. Creation of ResourceManager, RenderingEngine, and
ApplicationEngine
m_resourceManager = CreateResourceManager();
if (api == kEAGLRenderingAPIOpenGLES1) {
NSLog(@"Using OpenGL ES 1.1");
m_renderingEngine = ES1::CreateRenderingEngine(m_resourceManager);
} else {
NSLog(@"Using OpenGL ES 2.0");
m_renderingEngine = ES2::CreateRenderingEngine(m_resourceManager);
}
m_applicationEngine = CreateApplicationEngine(m_renderingEngine,
m_resourceManager);