What I want to achieve:
Drawing png files with alpha exactly as it appears originally, without transforming any pixel. This is because the image is very detailed and I don't want to lose any bit of information.
Animating those images by rotating them and moving. No scaling.
Actually I don't want to use any 3rd party libraries like cocos2d. I have been reading blogs about OpenGL ES, also checked Texture2D.m so I have basic idea about drawing primitives in 3D space. As far as I understand if I need to draw and animate an image(sprite?) I can just make rectangle and map texture. But the problem is that I want my png file appear exactly as original, not scaled or rotated.
What is the best technique to achieve points mentioned above? Drawing textured rectangle in orthogonal viewport? How to preserve original size/color of image?
Sorry if question is a bit messed up, I can clarify.
By your description, I don't understand why you'd use OpenGL ES. Using Quartz and layers would enable 1) drawing PNGs 2) rotating and moving them (even scaling if you wished). It would be easier than setting up an orthogonal projection in OpenGL + handling loading of images.
Now, if you really want to go OpenGL, yes, you should setup an orthogonal projection, with view size strictly equal to the screen size, and draw a rectangle of the exact size, with texture mapped with exact 0/1 coordinates. For the color aspect, you can use 8888 format, which is exact, no compression, no color reduction and with full alpha.
I know you said you don't want to use a third party library, but Cocos2D implements 2D graphics with OpenGL, so you can reference CCSprite.m to see how they did it. That said, you may want to consider if this library is appropriate for your application. In order to do your own 3D rendering all you have to do is extend CCSprite and put in your own code, as you can see in the comment below the state is already setup for you and everything.
CCSprite.m:
-(void) draw
{
NSAssert(!usesBatchNode_, #"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -
BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}
#define kQuadSize sizeof(quad_.bl)
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
long offset = (long)&quad_;
// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));
// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
#if CC_SPRITE_DEBUG_DRAW
CGSize s = [self contentSize];
CGPoint vertices[4]={
ccp(0,0),ccp(s.width,0),
ccp(s.width,s.height),ccp(0,s.height),
};
ccDrawPoly(vertices, 4, YES);
#endif // CC_TEXTURENODE_DEBUG_DRAW
}
Related
I would like to render image in OpenGL ES, pixel by pixel. I want to do it this way because I plan to move those pixels over time to create various effect.
For performance and design reasons I decided to use only every other pixel in both directions (thus reducing their number to one quarter)
I have only very basic understanding of opengl, so I am probably missing some key knowledge to achieve this.
What is the best way to achieve this? Do I have to really render it pixel by pixel? Or can I somehow create texture out of array of pixels?
I would like to make this work on as much devices as possible (so OpenGL ES 1.1 solution is preffered, but if it is not possible or it would be really inconvenient or slow, 2.0 can be used)
I tried to do this using VBO with mixed results. I am not sure I have done it properly, because there are some problems (and it is very slow). Here is my code:
Initialization:
glGenBuffers(1, &pointsVBO);
glBindBuffer(GL_ARRAY_BUFFER, pointsVBO);
glBufferData(GL_ARRAY_BUFFER, 160*240*sizeof(Vertex), 0, GL_DYNAMIC_DRAW);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
Rendering:
- (void)renderPoints:(ImagePixel**)imagePixels {
int count = 160 * 240;
for(int i = 0; i < count; ++i) {
vertices[i].v[0] = imagePixels[i]->positionX;
vertices[i].v[1] = imagePixels[i]->positionY;
vertices[i].color[0] = imagePixels[i]->red;
vertices[i].color[1] = imagePixels[i]->green;
vertices[i].color[2] = imagePixels[i]->blue;
vertices[i].color[3] = 1;
}
glVertexPointer(2, GL_FLOAT, sizeof(Vertex), vertices[0].v);
glColorPointer(4, GL_FLOAT, sizeof(Vertex), vertices[0].color);
// update vbo
GLvoid *vbo_buffer = glMapBufferOES(GL_ARRAY_BUFFER, GL_WRITE_ONLY_OES);
memcpy(vbo_buffer, vertices, count * sizeof(Vertex));
glUnmapBufferOES(GL_ARRAY_BUFFER);
// draw contents of vbo
glDrawArrays(GL_POINTS, 0, count);
}
Vertex struct:
typedef struct Vertex
{
float v[2];
float color[4];
} Vertex;
imagePixels array is filled with data from image.
When I do this, I get most of my image but I am missing few last rows and I can see some random pixels around the screen. Is it possible that I hit some limit in drawArrays that uses only portion of data?
Second problem is, that points in second half of columns aren't aligned properly. I guess this is caused by rounding errors in float math when computing position during rendering itself (supplied coordinates are all multiples of 2). Is there any way how to prevent this? I need all points to be aligned in proper grid.
I will provide you with screenshot as soon as I get my iphone back
If you really are wanting to manipulate every pixel, you should probably just use a single full-screen quad in OpenGL and update its texture each frame.
You can create a texture out of a bitmap array of pixels using glTexImage2D.
I would appreciate some help with the following. I'm trying to render a ring shape on top of another object in OpenGL ES 1.1 for an iPhone game. The ring is essentially the difference between two circles.
I have a graphic prepared for the ring itself, which is transparent in the centre.
I had hoped to just create a circle, and apply the texture to that. The texture is a picture of the ring that occupies the full size of the texture (i.e. the outside of the ring touches the four sides of the texture). The centre of the ring is transparent in the graphic being used.
It needs to be transparent in the centre to let the object underneath show through. The ring is rendering correctly, but is a solid black mass in the centre, not transparent. I'd appreciate any help to solve this.
Code that I'm using to render the circle is as follows (not optimised at all: I will move the coords in proper buffers etc for later code, but I have written it this way to just try and get it working...)
if (!m_circleEffects.empty())
{
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
int segments = 360;
for (int i = 0; i < m_circleEffects.size(); i++)
{
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(m_circleEffects[i].position.x, m_circleEffects[i].position.y, 0);
glBindTexture(GL_TEXTURE_2D, m_Texture);
float radius = 1.764706;
GLfloat circlePoints[segments * 3];
GLfloat textureCoords[segments * 2];
int circCount = 3;
int texCount = 2;
for (GLfloat i = 0; i < 360.0f; i += (360.0f / segments))
{
GLfloat pos1 = cosf(i * M_PI / 180);
GLfloat pos2 = sinf(i * M_PI / 180);
circlePoints[circCount] = pos1 * radius;
circlePoints[circCount+1] = pos2 * radius;
circlePoints[circCount+2] = (float)z + 5.0f;
circCount += 3;
textureCoords[texCount] = pos1 * 0.5 + 0.5;
textureCoords[texCount+1] = pos2 * 0.5 + 0.5;
texCount += 2;
}
glVertexPointer(3, GL_FLOAT, 0, circlePoints);
glTexCoordPointer(2, GL_FLOAT, 0, textureCoords);
glDrawArrays(GL_TRIANGLE_FAN, 0, segments);
}
m_circleEffects.clear();
glDisable(GL_TEXTURE_2D);
glDisable(GL_DEPTH_TEST);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
I've been experimenting with trying to create a ring rather than a circle, but I haven't been able to get this right yet.
I guess that the best approach is actually to not create a circle, but a ring, and then get the equivalent texture coordinates as well. I'm still experimenting with the width of the ring, but, it is likely that the radius of the ring is 1/4 width of the total circle.
Still a noob at OpenGL and trying to wrap my head around it. Thanks in advance for any pointers / snippets that might help.
Thanks.
What you need to do is use alpha blending, which blends colors into each other based on their alpha values (which you say are zero in the texture center, meaning transparent). So you have to enable blending by:
glEnable(GL_BLEND);
and set the standard blending functions for using a color's alpha component as opacity:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
But always keep in mind in order to see the transparent object correctly blended over the object behind, you need to render your objects in back to front order.
But if you only use the alpha as a object/no-object indicator (only values of either 0 or 1) and don't need partially transparent colors (like glass, for example), you don't need to sort your objects. In this case you should use the alpha test to discard fragments based on their alpha values, so that they don't pollute the depth-buffer and prevent the behind lying object from being rendered. An alpha test set with
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.5f);
will only render fragments (~pixels) that have an alpha of more than 0.5 and will completely discard all other fragments. If you only have alpha values of 0 (no object) or 1 (object), this is exactly what you need and in this case you don't actually need to enable blending or even sort your objects back to front.
I am currently working on an OPENGL ES 2.0 project. I am making a bar graph application. I have achieved success in making the graph. I am trying to insert text on the graph. I found out that there is no direct solution to achieve the same.
I came across a OPENGL ES 1.1 code, which has the following lines in it.
File: In GLViewController.m (from the drawView function)
Texture2D *textTex = [[Texture2D alloc] initWithString:#"Text"
dimensions:CGSizeMake(100., 40.0)
alignment:UITextAlignmentCenter
font:[UIFont boldSystemFontOfSize:10.0]];
[textTex drawAtPoint:CGPointMake(160.0, 100.0) depth:-1];
In the Texture2D.m file,
- (void) drawAtPoint:(CGPoint)point depth:(CGFloat)depth
{
GLfloat coordinates[] = {
0, _maxT,
_maxS, _maxT,
0, 0,
_maxS, 0
};
GLfloat width = (GLfloat)_width * _maxS,
height = (GLfloat)_height * _maxT;
GLfloat vertices[] = {
-width / 2 + point.x, -height / 2 + point.y, depth,
width / 2 + point.x, -height / 2 + point.y, depth,
-width / 2 + point.x, height / 2 + point.y, depth,
width / 2 + point.x, height / 2 + point.y, depth
};
glBindTexture(GL_TEXTURE_2D, texture.name);
glVertexPointer(3, GL_FLOAT, 0, vertices); //1
glTexCoordPointer(2, GL_FLOAT, 0, coordinates); //2
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
When I tried to convert this code into OPENGL ES 2.0 code, I am not able to find a suitable conversion for lines marked 1 and 2. Can someone please help me convert this code to OPENGL ES 2.0 code?
Or if an anyone is aware of an easier method to write text on screen, please let me know.
OpenGL ES 1.x and 2.x share a lot of concepts when it comes to geometry. You specify vertices, then you join them with geometry, which is turned into pixels. The difference is that you specify custom code in two places: to take the input geometry and figure out where it goes on screen and what stuff it passes on through the pipeline, and to figure out what colour an individual fragment is based on the information coming along the pipeline.
Because ES 1.x wasn't programmable in that sense, it had fixed functionality at both places. Which meant fixed data structures, and hence why you supply data via glVertexPointer, glTexCoordPointer, etc. You're supplying values for the fixed, predetermined fields that OpenGL has written into stone as describing a vertex.
Because ES 2.x is fully programmable and minimalist, it makes no assumptions about what fields describe a vertex and accordingly has no special case data provision methods, such as one to supply locations (glVertexPointer), another to supply texture coordinates (glTexCoordPointer), etc.
In ES 2.x you use glVertexAttribPointer to replace all of the fixed meaning ES 1.x calls. The first parameter identifies which attribute you're specifying by index, that index having been forced by yourself via glBindAttribLocation or left to figure themselves out and requested back via glGetAttribLocation.
You'll also need to write a suitable fragment and pixel shader, essentially just to pass the texture coordinates you nominate on, and to sample the texture per pixel, then pass that on. Should be just one or two lines in the body of each.
Probably you have some sort of framework in place for dealing with this stuff in your graphing. Because ES 2.x is so generic about attributes, it should be possible to reuse much the same stuff.
I have particles that I want to be able to change the color of in code, so any color can be used. So I have only one texture that basically has luminance.
I've been using glColor4f(1f, 0f, 0f, 1f); to apply the color.
Every blendfunc I've tried that has come close to working ends up like the last picture below. I still want to preserve luminance, like in the middle picture. (This is like the Overlay or Soft Light filters in Photoshop, if the color layer was on top of the texture layer.)
Any ideas for how to do this without programmable shaders? Also, since these are particles, I don't want a black box behind it, I want it to add onto the scene.
Here is a solution that might be close to what you're looking for:
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glActiveTexture( GL_TEXTURE0 );
glEnable( GL_TEXTURE_2D );
glBindTexture(GL_TEXTURE_2D, spriteTexture);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
glActiveTexture( GL_TEXTURE1 );
glEnable( GL_TEXTURE_2D );
glBindTexture(GL_TEXTURE_2D, spriteTexture);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD );
What it does is multiply the original texture by the specified color and then adds the pixels values of the original texture on top:
final_color.rgba = original_color.rgba * color.rgba + original_color.rgba;
This will result in a brighter image than what you've asked for but might be good enough with some tweaking.
Should you want to preserve the alpha value of the texture, you'll need to use GL_COMBINE instead of GL_ADD (+ set GL_COMBINE_RGB and GL_COMBINE_ALPHA properly).
Here are some results using this technique on your texture.
NONSENSE! You don't have to use multi-texturing. Just premultiply your alpha.
If you premultiply alpha on the image after you load it in and before you create the GL texture for it then you only need one texture unit for the GL_ADD texture env mode.
If you're on iOS then Apple's libs can premultiply for you. See the example Texture2D class and look for the kCGImageAlphaPremultipliedLast flag.
If you're not using an image loader that supports premultiply then you have to do it manually after loading the image. Pseudo code:
uint8* LoadRGBAImage(const char* pImageFileName) {
Image* pImage = LoadImageData(pImageFileName);
if (pImage->eFormat != FORMAT_RGBA)
return NULL;
// allocate a buffer to store the pre-multiply result
// NOTE that in a real scenario you'll want to pad pDstData to a power-of-2
uint8* pDstData = (uint8*)malloc(pImage->rows * pImage->cols * 4);
uint8* pSrcData = pImage->pBitmapBytes;
uint32 bytesPerRow = pImage->cols * 4;
for (uint32 y = 0; y < pImage->rows; ++y) {
byte* pSrc = pSrcData + y * bytesPerRow;
byte* pDst = pDstData + y * bytesPerRow;
for (uint32 x = 0; x < pImage->cols; ++x) {
// modulate src rgb channels with alpha channel
// store result in dst rgb channels
uint8 srcAlpha = pSrc[3];
*pDst++ = Modulate(*pSrc++, srcAlpha);
*pDst++ = Modulate(*pSrc++, srcAlpha);
*pDst++ = Modulate(*pSrc++, srcAlpha);
// copy src alpha channel directly to dst alpha channel
*pDst++ = *pSrc++;
}
}
// don't forget to free() the pointer!
return pDstData;
}
uint8 Modulate(uint8 u, uint8 uControl) {
// fixed-point multiply the value u with uControl and return the result
return ((uint16)u * ((uint16)uControl + 1)) >> 8;
}
Personally, I'm using libpng and premultiplying manually.
Anyway, after you premultiply, just bind the byte data as an RGBA OpenGL texture. Using glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); with a single texture unit should be all you need after that. You should get exactly (or pretty damn close) to what you want. You might have to use glBlendFunc(GL_SRC_ALPHA, GL_ONE); as well if you really want to make the thing look shiny btw.
This is subtly different from the Ozirus method. He's never "reducing" the RGB values of the texture by premultiplying, so the RGB channels get added too much and look sort of washed out/overly bright.
I suppose the premultiply method is more akin to Overlay whereas the Ozirus method is Soft Light.
For more, see:
http://en.wikipedia.org/wiki/Alpha_compositing
Search for "premultiplied alpha"
I'm developing painting app. I've tried to do it with CoreGraphics/Quartz 2D and drawing curves algorithm is pretty slow. So we've decided to switch to the OpenGL ES.
I've never had any OpenGL experience, so I found glPaint example from apple and started play with it.
I've changed erase method do make white background.
How I stuck with brushes and blending. In the example Apple uses "white on black" texture for the brush (first on the pic below). But it didn't work for me (I played with different blending modes). So I've decided to use different brushes, but I didn't find the proper way.
I found few questions on the stackoverflow, but all of them were unanswered. Here is a picture (from another question, thanks to Kevin Beimers).
(source: straandlooper.com)
So the question is how to implement stroke like "desired" in the picture. And how to blend 2 strokes closer to real life experience (blue over yellow = dark green).
Thanks.
There is current code (bit modified from glPaint) for the brush (from initWithFrame method:
// Make sure the image exists
if(brushImage) {
// Allocate memory needed for the bitmap context
brushData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
// Use the bitmatp creation function provided by the Core Graphics framework.
brushContext = CGBitmapContextCreate(brushData, width, width, 8, width * 4, CGImageGetColorSpace(brushImage), kCGImageAlphaPremultipliedLast);
// After you create the context, you can draw the image to the context.
CGContextDrawImage(brushContext, CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height), brushImage);
// You don't need the context at this point, so you need to release it to avoid memory leaks.
CGContextRelease(brushContext);
// Use OpenGL ES to generate a name for the texture.
glGenTextures(1, &brushTexture);
// Bind the texture name.
glBindTexture(GL_TEXTURE_2D, brushTexture);
// Set the texture parameters to use a minifying filter and a linear filer (weighted average)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Specify a 2D texture image, providing the a pointer to the image data in memory
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, brushData);
// Release the image data; it's no longer needed
free(brushData);
// Make the current material colour track the current color
glEnable( GL_COLOR_MATERIAL );
// Enable use of the texture
glEnable(GL_TEXTURE_2D);
// Set a blending function to use
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
// Enable blending
glEnable(GL_BLEND);
// Multiply the texture colour by the material colour.
glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
}
//Set up OpenGL states
glMatrixMode(GL_PROJECTION);
CGRect frame = self.bounds;
glOrthof(0, frame.size.width, 0, frame.size.height, -1, 1);
glViewport(0, 0, frame.size.width, frame.size.height);
glMatrixMode(GL_MODELVIEW);
glDisable(GL_DITHER);
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_VERTEX_ARRAY);
glEnable(GL_BLEND);
// Alpha blend each "dab" of paint onto background
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
//glBlendFunc(GL_SRC_COLOR, GL_ONE);
glEnable(GL_POINT_SPRITE_OES);
glTexEnvf(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_TRUE);
self.brushScale = 3;
self.brushStep = 3;
self.brushOpacity = (1.0 / 1.5);
glPointSize(width / brushScale);
//Make sure to start with a cleared buffer
needsErase = YES;
[self erase];
Let’s start by defining the type of blending you’re looking for. It sounds like you want your buffer to start out white and have your color mixing obey a subtractive color model. The easiest way to do that is to define the result of mixing Cbrush over Cdst as:
C = Cbrush × Cdst
Notice that using this equation, the result of mixing yellow (1, 1, 0) and cyan (0, 1, 1) is green (0, 1, 0), which is what you’d expect.
Having a brush that fades at the edges complicates things slightly. Let’s say you now have a brush opacity value Abrush—where Abrush is 1, you want your brush color to blend at full strength, and where Abrush is 0, you want the original color to remain. Now what you're looking for is:
C = (Cbrush × Cdst) × Abrush + Cdst × (1 - Abrush)
Since blending in OpenGL ES results computes C = Csrc × S + Cdst × D, we can get exactly what we want if we make the following substitutions:
Csrc = Cbrush × Abrush
Asrc = Abrush
S = Cdst
D = (1 - Abrush)
Now let’s look at what it takes to set this up in OpenGL ES. There are 4 steps here:
Change the background color to white.
Change the brush texture to an alpha texture.
By default, GLPaint creates its brush texture as an RGBA texture with the brush shape drawn in the RGB channels, which is somewhat unintuitive. For reasons you’ll see later, it’s useful to have the brush shape in the alpha channel instead. The best way to do this is by drawing the brush shape in grayscale with CG and creating the texture as GL_ALPHA instead:
CGColorSpaceRef brushColorSpace = CGColorSpaceCreateDeviceGray();
brushData = (GLubyte *) calloc(width * height, sizeof(GLubyte));
brushContext = CGBitmapContextCreate(brushData, width, width, 8, width, brushColorSpace, kCGImageAlphaNone);
CGColorSpaceRelease(brushColorSpace);
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, brushData);
Set up Csrc, Asrc, S and D.
After switching to an alpha texture, assuming that your brush color is still being specified via glColor4f, you’ll find that the default OpenGL ES texture environment will give you this:
Csrc = Cbrush
Asrc = Abrush
In order to obtain the extra multiplication by Abrush for Csrc, you’ll need to set up a custom combiner function in the texture environment as follows (you can do this in the initialization function for PaintingView):
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_ALPHA);
Changing GL_TEXTURE_ENV_MODE to GL_COMBINE gives you Cbrush × 0 (to see why this is the case, read section 3.7.12 in the OpenGL ES 1.1 specification). Changing GL_OPERAND0_RGB to GL_SRC_ALPHA changes the second term in the multiplication to what we want.
To set up S and D, all you need to do is change the blending factors (this can be done where the blending factors were set up before):
glBlendFunc(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA);
Ensure that any modifications to Abrush outside of the brush texture are reflected across other channels.
The above modifications to the texture environment only take into account the part of the brush opacity that come from the brush texture. If you modify the brush opacity in the alpha channel elsewhere (i.e. by scaling it, as in AppController), you must make sure that you make the same modifications to the other three channels:
glColor4f(components[0] * kBrushOpacity, components[1] * kBrushOpacity, components[2] * kBrushOpacity, kBrushOpacity);
Note that the downsides to implementing your brushes with a subtractive color model are that colors can only get darker, and repeatedly drawing the same color over itself can eventually result in a color shift if it’s not one of the primary subtractive colors (cyan, magenta, or yellow). If, after implementing this, you find that the color shifts are unacceptable, try changing the brush texture to an alpha texture as in step 2 and changing the blend factors as follows:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
This will give you simple painting of your brush color over white, but no actual mixing of colors (the brush colors will eventually overwrite the background).