iOS | How-to Tutorial on Developing iPad & iPhone Apps using the OpenGL and GLKit Frameworks

GLKit is a new framework to make working with OpenGL ES 2.0 a lot easier. The framework includes a set of classes to implement OpenGL ES. To start, what is OpenGL ES anyway? Good question. OpenGL ES is an API to render full 2D and 3D graphics in embedded systems - this includes gaming consoles, medical devices, industrial equipment, vehicles, you name it. The Open GL ES (Embedded System) is also a subset of the Desktop OpenGL, which is the full version for graphic acceleration by providing a stable interface with graphic acceleration hardware.

With previous versions of the SDK, you had to write all the heavy lifting code like shading and buffering or to add any special effects. With the GLKit you need only implement the GLKView or the GLKViewController, for example the GLKView implements the framebuffer so you only need to write to the framebuffer to update your contents. Likewise with the GLKViewController implements all the rendering loops and works with the GLKitView to do most of the work of managing the OpenGL ES rendering.

In addition to the GLView and GLKViewController, you have several other important classes to work with. One of them is the GLKBaseEffect.

GLKBaseEffect

This class is primarily responsible for implementing OpenGL ES 1.0 shaders and lighting. The class has several constants to help layout the lighting, shading and material of the object to be drawn on screen. To implement this class, you need to prepare your own vertices, or vertex data which will be rendered through the GLKView or GKLViewController. Another important class is the GLKSkyBoxEffect.

You can enable up to 3 lights and the lightning type. The types can include: GLKLightingTypePerVertex, GLKLightingTypePerPixel. You can also specify the material which calculates to light values. The transform contains to model view matrix which set the scene of the lighting. As the lighting changes, transform updates the model view matrix.

GLKSkyBoxEffect

Unlike the previous effect class, this class doesn't require any custom vertices as it creates its own at initialization time. You would need to set some properties like label, which is used to identify the object but it is not used by the shader class for its rendering, rather it can be used to send output to the screen or to the NSLog for instance. Like the previous effect class, you would call the prepareToDraw method to render. To actually output the image of the GLKSkyBox, you would call the draw method after the prepareForDraw.

Other properties that are required are:

textureCubeMap

This property sets the texture, of type GLKEffectPropertyTexture,of the SkyBox object. The GLKEffectPropertyTexture uses an input color to which a tranformatio is applied and sampled to provide an output color to be used as the texture in the textureCubMap. Calling the default GL_True will enable to application of the texturing, otherwise to can set the enabled propety to false, or GL_False.

Additionally you would set the envMode, which is the type of texture to apply to the map. There are three envMode types: GLKTextureEnvModeReplace, GLKTextureEnvModeModulate, GLKTextureEnvModeDecal.

Another property that can be set to define your textured map is the target. This properties' default is the GLKTexturedTarget2D and it creates a 2d map. The other options include the GLKTextureCubeMap, a texture encapsulating six textures types to form a cube map, and GLKTextureTargetCt, which returns an enumeration of textures.

center

This property set the x, y, z coordinates of your GLKSkyBox object on screen. You set this propery using a GLKVector3 type, which is a three prong floating point vector.

xSize

The property sets width of the SkyBox.

ySize

This property sets the height of the SkyBox.

zSize

The zSize property sets the depth of the SkyBox and completes the three dimensional representation of the object.

transform

The transform task sets the camera angle and position. It is a read-only property of a GLKEffectPropertyTransformtype.

GLKTextureLoader

This class helps you load the different texture that can be applied to the GLKBaseEffect or GLKSkyBoxEffect. You can also use the GLKTextureInfo to obtain information on the textures that can applied to the object being rendered. Using the GLKTextureLoader, you can load either 2d or cubed textures as previously mention into the textureCubeMap in the GLKSkyBoxEffect effect. These different textures can be applied to non mipmapped textures, which are pre calculated images (mips) that can be applied to a texture.

  • GL_TEXTURE_MIN_FILTER:GL_LINEAR
  • GL_TEXTURE_MAG_FILTER:GL_LINEAR
  • GL_TEXTURE_WRAP_S:GL_CLAMP_TO_EDGE
  • GL_TEXTURE_WRAP_T:GL_CLAMP_TO_EDGE

The mipmapped textures that can be defined are:

  • GL_TEXTURE_MIN_FILTER:GL_LINEAR_MIPMAP_LINEAR
  • GL_TEXTURE_MAG_FILTER:GL_LINEAR
  • GL_TEXTURE_WRAP_S:GL_CLAMP_TO_EDGE
  • GL_TEXTURE_WRAP_T:GL_CLAMP_TO_EDGE

Mipmap or Mip maps are used in 3D graphics to help redices aliases and to rendering speed.

Sample App

To demonstrate how to implement GLKit and show how ti use some of the concepts discussed in this section, I will create a simple app to draw a simple shape that bounces on screen. Start by creating a Single View application and delete the Viewcontroller and replace it a GLKViewController. You will naturally need to add the GLKit framework and the OpenGLES framework to your project so that you may use their classes in the project.

1-header

Open the ViewController class header file and change its parent class to GLKViewController. While there add a GLKViewDelegate and a GLKViewControllerDelegate. These delegates allow you to implement the clases methods without requiring to implement the class itself. In our case, the methods that we will need to implement are the glkView from the GLKView class and the glkViewControllerUpdate for the GLKViewController.

You will see how easy it is to implement OpenGL ES with the GLKView and GLKViewController.

You will need some instance variables to create the required functionality. See listing 1 below.

Code Listing 1-Declaring Instance Variables to Create an Animate Image

@property(nonatomic, strong) GLKBaseEffect * bEffect;
@property(nonatomic, strong) EAGLContext * ctx;
@property(nonatomic, strong) GLKView * graphicViewer; 
  • The bEffect instance variable of type GLKBaseEffect will setup the object to be drawn on screen. I will discuss its implementation in the next section. The EAGLContext ctx instance variable is the context manages the state of the GLKView, its commands and its different operations. Finally the GLKView * graphicViewer is our GLKView, to which you will assign the context and bEffect.
  • Since we are using the GLKViewDelegate and the GLKViewController Delegate, you will also need by extension, also implement the glkView instance method and the glkViewControllerUpdate instance method. These will both be called automatically to draw and update our object in the GLKView on screen.
  • As with the header file, add the GLKit header file to the implementation file. Next you are going to do some setup work in the viewdidLoad instance method of the GLKViewController as it inherits from the ViewController base class.
  • Start by initializing the context objet, ctx, by passing the initWithAPI parameter the kEAGLRenderingAPIOpenGLES2 value to signify that you will be using version 2.0 of the OpenGL ES API as in listing 2.

Code Listing 2-Initializing the CIContext variable

ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
  • Next you will need to initialize the GLKView, graphicViewer by setting it to its self and setting its context and delegate properties. See listing 3.

Code Listing 3-Assigning Values to the CIContext and Delegate

graphicViewer = (GLKView *) self.view;
graphicViewer.context = ctx;
graphicViewer.delegate = self;
  • The following three properties will establish the renderBuffers that you will use. In the following code example you will use the drawableColorFormat, which will be set to GLKViewDrawableColorFormatRGB565. You could also use the GLKViewDrawableColorFormat8888 value, encapsulating 8 bits for each of the base colors. The next property to set in the example app, is the drawableStencilFormat. This one of several rendering properties for the GLKView and its we will use the GLKViewDrawableStencilFormat8 value; a 8-bit stensil. Finally we will use GLKViewDrawableDepthFormat16 value for the drawableDepthFormat property as in listing 4.

Code Listing 4-Assigning Values to the Drawable Properties

    graphicViewer.drawableColorFormat = GLKViewDrawableColorFormatRGB565;
    graphicViewer.drawableStencilFormat = GLKViewDrawableStencilFormat8;
    graphicViewer.drawableDepthFormat = GLKViewDrawableDepthFormat16;
  • Now that we have defined our depth, color and stensil properties, we can configure the GLKViewController's frames per second. We set this to 60, the maximum, but it is up to you define the needed value to support your design purposes. This is rate at which the GLKViewControlled refreshes the display on screen. The property is called preferedFramesPerSecond and the GLKViewController uses it as a guideline only. The default is 30. See listing 5.

Code Listing 5-Setting the preferredFramesPerSecond property

self.preferredFramesPerSecond = 60;
  • Next we need to set the GLKViewController's delegate to it self, otherwise object on screen will be stationary as the the glkViewControllerUpdate method won[nd]t be called. The final line of code will initialize the bEffect instance variable that will be used later. Listing 6.

Code Listing 6-Setting the delegate and BaseEffect variable

  self.delegate=self;
  bEffect = [[GLKBaseEffect alloc] init];

This completes the code for the viewDidLoad instance method. The three parts, which include the glkView, the glkViewControllerUpdate and to cap off the viewDidUnload to clean up our code.

Implement the glkView

This method permits us to define what the object will look like in the GLKView. For starters, we will need to call the prepareToDraw method, which is part of the GLKView lifecycle that is require to render any 2d or 3d objects on screen.

  • To start we will define the dimensions of our object. How you setup the dimensions is pretty much up to you. You create your shapes by combining an array of coordinates to create the shape or shapes as required to produce the desired object. In the code in listing 7 we are setting the four cardinal points of our trapezoid by providing the points of two triangles that will be stored in an array of type GLfloat.

Code listing 7 - Trapezoid Coordinates

    static const GLfloat Vertices[] = {
        -1.5f, -1.5f, 1,
        0.5f, -0.5f, 1,
        -0.5f,  0.5f, 1,
        0.5f,  0.5f, 1
    };
  • Next we will define the colors to use in our object. The colors are another array of GLubyte data type. GLubyte is an unsigned literal of type char. The openGL framework defines it as an 8-bit integer.In listing 8 we define an array of four dimensions.

Code Listing 8 - Defining the RBG Colors

//This the RGB colors to draw
    static const GLubyte Colors[] = {
        255, 255,   0, 125,
        0,   255, 255, 255,
        0,   255,   0,   255,
        255,   0, 255, 255,
    };
  • Before we can use these arrays to draw our object, we will need to do some housecleaning by first clearing any colors, listing 9, in the buffer using the glClearColor to reset the color buffers. The glClearColor sets the red, green, blue and alpha values. In the following example, we are clearing all colors so our background will be black. Setting one of the first three to 1.0f would clear the corresponding reg, green or blue colors plus alpha.

Code Listing 9 - Defining Color Buffer to Clear

 glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

Follow this with the glClear, listing 10, to actually clear the buffers to their preset values. When called, the glClear resets the values in the bitplane to one of the buffer types. Since we are using three buffers: color, depth and stensil, we will also clear these buffers:

Code Listing 10 - Using the glClear method to clear the Color Buffers

glClear(GL_COLOR_BUFFER_BIT);
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_STENCIL_BUFFER_BIT);

Now that the coordinates and colors have been set and our housecleaning done, we can actually draw our image on screen.

  • The first step is to use the glEnableVertexAttribArray, listing 11, to enable the vertex attributes that we are using. In our case, we are using the vertices and colors, so we need to enable both these attributes in the vertex array. The GLKVertexAttribPosition is an index to the shader position in our vertex. Likewise the GLKVertexAttribColor the index of the color that will be provided to the shader for our vertex.

Code Listing 11 - Enable the Vertex Attributes

glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribColor);

These properties are more useful when developing games and can be disabled or enabled as required.

  • Next in our implementation of the object is to define our pointers for the vertices and colors, listing 12. This Vertices array will be used with the glVertexAttribPointer openGL function to define our vertex data.

Code Listing 12 - Defining the Vertex Pointer

glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, Vertices);

The glVertexAttribPointer defines our vertex data which includes the coordinates of our vertex, defined by the vertices array from above. Again we will use the GLKVertexAttribPosition since the first parameter is an index. The second parameter is the size or number of components in the vertex. You can have up to 4 components with 4 being the initial value. We will use 3 in our example. The third argument is the data type of our vertex data, which we have already stated as being GLfloat. The fourth argument sets the normalisation of the floating point of the data. Specifying false converts the data directly to fixed-point values. The stride argument of fifth parameter , 0, which is the default, determines the byte offset of the vertices. Finally the last argument is our data.

We will repeat the process for the colors, listing 13, and we will use the same arguments, except for the fourth argument, which is the normalisation. We will normalise this by passing a GL_True value. The last value will be our array of colors to use in our object. The line of code will actually output our object to the GLKView.

Code Listing 13 - Setting the Color Array Pointer

  glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, Colors);

The first parameter of listing 14 determines the actual shape our image will have. there are several to choose from. In this example, we will use the GL_TRIANGLE_FAN, but could also use from several others. The second argument is the starting point which as will any array is 0. Finally the last argument is the numer of indices to use when rendering the data.

Code Listing 14 - Defining the Shape to Draw

    /* Sets the arrays of the object to be drawn.
     The first parameter is one these mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, 
     GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS,
     and GL_POLYGON.
     The second parameter is the starting point of the array, and the third is the number 
     of vertices to include in the rendering.
     */
    glDrawArrays(GL_TRIANGLE_FAN, 0, 10);

Finally since we enabled the vertex attributes, listing 15, we will disable them here as well:

Code Listing 15 - Enabling the Vertex Attributes

glDisableVertexAttribArray(GLKVertexAttribPosition);
glDisableVertexAttribArray(GLKVertexAttribColor);

Here is the output of all our hard work:

Figure 1 -  GLkit sample output of a GL_TRANGLE_FAN
Figure 1 - GLkit sample output of a GL_TRANGLE_FAN

We could stop here but our image would simple be a static image on screen. Our next method, the glkViewControllerUpdate will actually recalculate our image on screen and provide movement. For this simple example, we will make the image bounce on screen.

Code Listing 16 - The Complete glView Implementation

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
    [bEffect prepareToDraw];
    
    static const GLfloat Vertices[] = {
        -1.5f, -1.5f, 1,
        0.5f, -0.5f, 1,
        -0.5f,  0.5f, 1,
        0.5f,  0.5f, 1
    };
    
    
    //This the RGB colors to draw
    static const GLubyte Colors[] = {
        255, 255,   0, 125,
        0,   255, 255, 255,
        0,   255,   0,   255,
        255,   0, 255, 255,
    };
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glClear(GL_DEPTH_BUFFER_BIT);
    glClear(GL_STENCIL_BUFFER_BIT);
    
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, Colors);
    
    /* Sets the arrays of the object to be drawn.
     The first parameter is one these mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, 
     GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS,
     and GL_POLYGON.
     The second parameter is the starting point of the array, and the third is the number 
     of vertices to include in the rendering.
     */
    glDrawArrays(GL_TRIANGLE_FAN, 0, 10);
    
    
    
    glDisableVertexAttribArray(GLKVertexAttribPosition);
    glDisableVertexAttribArray(GLKVertexAttribColor);
}

Implement the glkViewControllerUpdate Method

The glkViewControllerUpdate is a required method in the glViewDelegate protocol. It is called before each frame is displayed. A typical use would be to update the state information on the each frame of the object.

For our sample application glkViewControllerUpdate method will move the object to each new set of coordinated. Listing 17 is the complete implementation of the glkViewControllerUpdate of the GLKViewController

  • The first three lines of code obtains the boundaries of the drawing area. The higher of the y position, posY, the faster the image will move. The next line will define a GLKMatrix4 instance variable.
  • We will need to define and provide a model view and projection for our object to provide its animation. We will implement the modelview first by using the GLKMatrix4 class. The GLKMatrix4 is 4x4 matrix and has a wide array of functions allowing a developer to apply many different types of mathematical operations. We will use the GLKMatrix4MakeTranslation method. The method requires three arguments, the x position or 0, the y position which will provided using the y variable and the z position that we will be -0.5f).
  • We will then apply the GLKMatrix4 matrix to our bEffect or GLKBaseEffect to provide the model view projection.
  • Next we will to define a projection which will transform our positioning coordinates on screen. Here in our example I will use a GLKMatrix4MakePerspective, but could have used the GLKMatrix4MakeYRotation or GLKMatrix4MakeXRotation to make the object rotate on its y or x axis.

Finally we apply the projection to our bEffect and we are done.

Code Listing 17 - glkViewControllerUpdate Implementation

static float posY = 0.0f;
float y = sinf(posY)/2.0f;
posY += 0.255f;
GLKMatrix4 modelviewmatrix = GLKMatrix4MakeTranslation(0, y, -5.0f);
bEffect.transform.modelviewMatrix = modelviewmatrix;

GLfloat ratio = self.view.bounds.size.width/self.view.bounds.size.height;
GLKMatrix4 projection = GLKMatrix4MakePerspective(45.0f, ratio, 0.1f, 20.0f);
bEffect.transform.projectionMatrix = projection;

viewDidUnload

To clean our objects, we will add a couple lines of code to reset our context and set our graphicView to nil as in listing 18.

The GLKit and by extension the openGL ES are very powerful frameworks for high quality 2d and 3d graphics and animation. In this section, we barely skimmed the surface. These two subjects could easily fill a couple of books on their own and I sincerely invite you all, if you are interested in animation to further your explorations by visiting the openGL web site and the GLKIt and openGL ES sections on the Apple Developer web site.

Code Listing 18 - Resetting the Variables


[self setGraphicViewer:nil];

[EAGLContext setCurrentContext:nil];

[self setCtx:nil];

In Conclusion

The open GL ES framework and the GLKit offer a powerful set of libraries for 2d and 3d animation. As we push the boundaries in medical output radar and environmental analysis, business analysis, these libraries and others like it will leave their gaming roots to venture into vast new directions and applications.


Live Demo of running App

Code for klViewController.h

//
//  klViewController.h
//  GLKit_App
//
//  Created by Kevin Languedoc.
//  Copyright (c) 2012 Kevin Languedoc. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>



@interface klViewController : GLKViewController<GLKViewDelegate, GLKViewControllerDelegate>

@property(nonatomic, strong) GLKBaseEffect * bEffect;
@property(nonatomic, strong) EAGLContext * ctx;
@property(nonatomic, strong) GLKView * graphicViewer;

/*These two methods must be implemented as they are part
 of the GLKViewController and will render the image
 on screen.*/
-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect;
-(void)glkViewControllerUpdate:(GLKViewController *)controller;


@end

Code for klViewController.m

//
//  klViewController.m
//  GLKit_App
//
//  
//  Copyright (c) 2012 Kevin Languedoc. All rights reserved.
//

#import "klViewController.h"
#import <GLKit/GLKit.h>



@implementation klViewController
@synthesize ctx, bEffect, graphicViewer;

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    /*Since we are using the OpenGL ES version 2.0 immplementation
     we will use the kEAGLRenderingAPIOpenGLES2 to specify to use
     the OpenGL Es 2.0 version, otherwise use the kEAGLRenderingAPIOpenGLES1
     value for version 1.0.
     */
    ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    graphicViewer = (GLKView *) self.view;
    graphicViewer.context = ctx;
    graphicViewer.delegate = self;
    
    graphicViewer.drawableColorFormat = GLKViewDrawableColorFormatRGB565;
    graphicViewer.drawableStencilFormat = GLKViewDrawableStencilFormat8;
    graphicViewer.drawableDepthFormat = GLKViewDrawableDepthFormat16;
   
    self.preferredFramesPerSecond = 60;
    self.delegate=self;
    bEffect = [[GLKBaseEffect alloc] init];
    
    
}



- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
    [bEffect prepareToDraw];
    
    static const GLfloat Vertices[] = {
        -1.5f, -1.5f, 1,
        0.5f, -0.5f, 1,
        -0.5f,  0.5f, 1,
        0.5f,  0.5f, 1
    };
    
    
    //This the RGB colors to draw
    static const GLubyte Colors[] = {
        255, 255,   0, 125,
        0,   255, 255, 255,
        0,   255,   0,   255,
        255,   0, 255, 255,
    };
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glClear(GL_DEPTH_BUFFER_BIT);
    glClear(GL_STENCIL_BUFFER_BIT);
    
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, Colors);
    
    /* Sets the arrays of the object to be drawn.
     The first parameter is one these mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, 
     GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS,
     and GL_POLYGON.
     The second parameter is the starting point of the array, and the third is the number 
     of vertices to include in the rendering.
     */
    glDrawArrays(GL_TRIANGLE_FAN, 0, 10);
    
    
    
    glDisableVertexAttribArray(GLKVertexAttribPosition);
    glDisableVertexAttribArray(GLKVertexAttribColor);
}

- (void)glkViewControllerUpdate:(GLKViewController *)controller{
    static float posY = 0.0f;
    float y = sinf(posY)/2.0f;
    posY += 0.355f;
    
    GLKMatrix4 modelviewmatrix  = GLKMatrix4MakeTranslation(0, y, -5.0f);
    bEffect.transform.modelviewMatrix = modelviewmatrix;

    GLfloat ratio = self.view.bounds.size.width/self.view.bounds.size.height;
    GLKMatrix4 projectionmatrix = GLKMatrix4MakePerspective(45.0f, ratio, 0.1f, 20.0f);
    bEffect.transform.projectionMatrix = projectionmatrix;
  
}

- (void)viewDidUnload
{
    [self setGraphicViewer:nil];
    [EAGLContext setCurrentContext:nil];
    [self setCtx:nil];
    
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
	[super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
	[super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return YES;
}

@end

More by this Author


Comments 2 comments

Henry J 2 years ago

Very helpful! Thanks for taking the time and writing it


klanguedoc profile image

klanguedoc 2 years ago from Canada Author

Thanks Henry. I appreciate the feedback. Kevin

    Sign in or sign up and post using a HubPages Network account.

    0 of 8192 characters used
    Post Comment

    No HTML is allowed in comments, but URLs will be hyperlinked. Comments are not for promoting your articles or other sites.


    Click to Rate This Article
    working