On-the-fly image edit, OpenGL Pros wanted

BlitzMax Forums/BlitzMax Programming/On-the-fly image edit, OpenGL Pros wanted

Dubious Drewski(Posted 2007) [#1]
Using the search, I can see that many people have wanted to
manipulate pixels on the fly. Many have suggested "Maybe
you could use some clever OpenGL"

Well I would love to manipulate pixels on the fly using
some clever OpenGL. And I would be ever so grateful if
someone could give me some hints as to what OpenGL commands
will allow me to manipulate the pixels of a TImage?

Something like
glbindtexture to select my image
then maybe glEnable(GL_TEXTURE_2D) to allow for texturing...
Is that good so far? Then what could I do?

Something like

glColor3ub(r,g,b)
glTexCoord2f(x,y)

?

Any advice would be nice.

[edit] I've always been very disappointed that Bmax can't edit images
without going through the fatally-slow process of converting to pixmaps.


Dubious Drewski(Posted 2007) [#2]
Can glReadPixels help?


Chris C(Posted 2007) [#3]
What are you *actually* trying to achieve, what specific effect?


Dubious Drewski(Posted 2007) [#4]
I have a single image as a background with entities running
around on this, digging, etc. So this background image (which
is at screen resolution) needs to be editable in realtime.

In this thread, I drew a nice explanation in photoshop which
explains exactly what I'm after (pics near the bottom).

Lockimage creates a pixmap of the entire image, and
loadImage(pixmap) is the part that's dreadfully slow. What
I could settle for is a Timage-to-Pixmap function that only
grabs a specified chunk of an image.

I think that's what I can do with glReadPixels, I just need
some help figuring out how to write a function that takes an
image and pastes it into another larger image afterwards.


ImaginaryHuman(Posted 2007) [#5]
In OpenGL, there is a thing called the `current texture object` which is sort of like an internal state that says which texture is the `current` one to use. To set that variable, ie to select what the current texture is, you `bind` (associate) a given texture with that current texture variable. The way they do it in OpenGL is to say that you bind a given texture handle to one of the `current` variables, ie if you do glBindTexture(MyTexture,GL_TEXTURE_2D) it is the same thing in basic as saying CurrentTexture=MyTexture. So you have to do that to set a current texture before you can start to draw textured objects. Reason being that when it comes to drawing a textured object, you don't want to have to pass a parameter for every polygon saying which texture to use. It uses whatever is the current one - ie which one you bound to the 2d-texturing selection.

Then to allow textures to be used in the coloring of pixels, you have to do glEnable(GL_TEXTURE_2D), like you said. This swithes texturing on, and uses whatever the GL_TEXTURE_2D says is the current texture.

To then draw, say, a textured quad/rectangle, you first start by telling OpenGL that you are starting the definition of a quad, as follows:

glBegin(GL_QUADS)
...
glEnd(GL_QUADS)

Then between these two commands, you put the definition of each vertex ie each corner of the quad. Usually you should define them in an anticlockwise direction, or at least be consistent with which way you do it.

When you use glColor to set a color, it doesn't actually DO anything other than say `CurrentColor=xyz`. You won't see the effect of it until you draw something. You can either define the color inside the quad declaration or outside it or on a per-quad basis (you can define more than one within the glbegin/glend). Normally, for the color to have no effect on what color the pixels end up as, and since it acts as a `tint` (as in SetColor r,g,b), if you just want the pixels to be the color they are in the texture then you just set the color to white. I usually use glColor4ub(r,g,b,a) (ub means unsigned byte). Don't use `b` byte because it's signed and will screw up your colors. On the other hand if you want to give each corner of the rectangle a different tint, you can set the color (before defining the each corner) just prior to the glVertex() command, which is what defines the actual corner. You can also give each corner a different ALPHA value, and so long as you SetBlend AlphaBlend it should let you make each corner have a different transparency. Since Max is set up by default to have smooth blending across the rectangle (gauraud shading/interpolation), the amount of color and the amount of alpha value will gradually smoothly transition across the whole rectangle based on the values at each corner.

The second thing you'll want to do is set the `current texture coordinate` that will be applied to the current corner. Remember that texture space has a coordinate system of 0 to 1.0. It doesn't go in pixel values. So if your texture is 640 pixels width, to refer to a coordinate on the far right edge would be 1.0, whereas the left edge is 0. I think the top edge is also 1.0 and bottom edge is 0 (it has a bottom left origin). So let's say you wanted to make the top left corner of your rectangle be in the top left corner of the screen, you'd set the texture coordinate to 0,1.0 and then when you draw the rectangle, 0,1 in the texture will be `locked onto` wherever you say the top left corner of the rectangle is. glTexCoord2f() defines the coordinate within the texture, within the 0-1.0 coordinate system, based on two floats (x and y).

The third step is to then define the coordinates of the corner of the rectangle. I usually do it in the order topleft,bottomleft,bottomright,topright anticlockwise. It has to be in sequence. So you say glVertex2f(x,y) which for the top left corner of the screen would be glVertex2f(0,0). Remember the vertex coordinates are (as set up in blitzmax by default) pixel coordinates. That's because BlitzMax told the system that the topleft corner is 0,0 and the bottom right is screenwidth,screenheight (it doesn't have to be that way, but that makes sense from a 2d point of view). glVertex2f means define a vertex (a point/corner) using 2 floats.

So it looks something like this:

glBindTexture(MyTextureHandle,GL_TEXTURE_2D) 'set current texture to use
glEnable(GL_TEXTURE_2D) 'switch on using textures for drawing pixels
glBegin(GL_QUADS) 'start defining some rectangles
   glColor4ub($FF,$FF,$FF,$FF) 'set current color to white
   glTexCoord2f(0,1.0) 'anchor topleft corner of texture to this corner
   glVertex2f(0,0) 'Define corner of rectangle in current color and anchored to the current texture coordinate
   '(so that pixel within that texture appears at this coordinate on the screen)
   'And continue with the other 3 corners...
   glColor4ub($FF,$0,$0,$FF) 'set current color to red
   glTexCoord2f(0,0) 'anchor bottom left of texture to this corner
   glVertex2f(0,ScreenHeight-1) 'Define bottom left with current color & texture coord
   glColor4ub($0,$FF,$0,$80) 'set current color to transparent green
   glTexCoord2f(1.0,0) 'anchor bottom right of texture to this corner
   glVertex2f(ScreenWidth-1,ScreenHeight-1) 'Define bottom right corner
   glColor4ub($0,$0,$FF,$FF) 'set current color to blue
   glTexCoord2f(1.0,1.0) 'anchore top right of texture to this corner
   glVertex2f(ScreenWidth-1,0) 'Define top right corner
glEnd()
Flip()

That should draw a texture mapped onto a rectangle covering the whole screen with different colors at each corner and different alpha values at each corner.

The nice thing then is if you move around the coordinates that you pass to glVertex2f, without changing the texture coordinates, the texture will stretch/shrink/warp to fill the new shape, because the corners of the texture are still anchored to the corners of the rectangle. If you wanted to make an irregular shaped quad and keep your texture from stretching, you have to position your texture coordinates to match.

This will enable you to draw the texture. What you need is to be able to modify the texture and to be able to draw the modified texture again in the next frame. Since you can't preserve the backbuffer content itself between frames you have to get the backbuffer into a texture. Either you can use extensions that allow you to draw directly to textures, which is somewhat more advanced and not as widely supported, or you draw the current `land` to the backbuffer and then draw the changes to it and then re-grab it back into a texture. That's what GrabImage does. To do it yourself with OpenGL you use glCopyTexSubImage2d(). This copies an area of the backbuffer (which doesn't have to be the whole thing) into the currently bound texture at given coordinates. You may have to refer to glRasterPos() to set where within the backbuffer you read from and then you define where within the texture you copy it to. This saves your changes. You should save changes before you start drawing other objects and game characters on top of it. Then the next frame when you draw your texture again as your background it will have the changes from the last frame. glCopyTexSubImage2d() is the fastest OpenGL 1.1-supported non-extension-oriented way of getting pixels into a texture.

You really should avoid using glReadPixels or glDrawPixels or use of pixmaps. Since you said you only have one screen of graphics and no scrolling you can easily just use one texture to contain the whole `level` of your game. You do not want to be transferring quantities of pixels over the graphics bus between main memory and the graphics card which is what you would be doing using pixmaps. It is slow because it has overhead and because the bus transfer is nowhere near as fast as the transfer of texture data from video ram to the backbuffer in video ram, by the hardware. glReadPixels, glDrawPixels, and indeed GrabPixmap or DrawPixmap, all use the CPU to transfer the data and does not use the graphics hardware. ie Slow. On my iMac drawing pixmaps from main memory is at least 10 times slower than drawing a texture from video ram. The faster your graphics card is the faster it will draw textures and also the grab is much faster too. You should be able to grab the whole screen into a texture on a per-frame basis and still maintain a high framerate 30-60fps at least.

The only other different from not using pixmaps is you use graphics commands to have the hardware draw your changes to what is being dug and so on, which in itself is faster and easier than writing your own pixmap modification code. If you do use pixmaps look at using pointers to change stuff, it's faster than WritePixel and ReadPixel. But once you try the above OpenGL technique I don't think you'll think twice about using pixmaps for this type of engine.


FlameDuck(Posted 2007) [#6]
I could settle for is a Timage-to-Pixmap function that only grabs a specified chunk of an image.
Here's an idea. Split up your background into tiles. You probably don't want to be loading "images" much bigger than 1024 x 1024 into memory anyway.


Dubious Drewski(Posted 2007) [#7]
@Flameduck: I thought of that, and it is a fallback option. I
want to avoid it because of the complications that would arise
when a single terrain-edit affects two or more adjacent tiles
at once. It's still an option, for sure, but I want to see if
this (probably simpler) method will work first.

@AngelDaniel: Holy wow. Thank you for the effort! I'm now
going to have a read through that monster post of yours before
I can respond to you. Give me a minute or two (or 20).


Chris C(Posted 2007) [#8]
looking at that other post I'd go the tile route, You can the also use the tiles for your a* map, you can use alpha blending on the edges and theres no reason why some tiles couldn't have several layers.

You could look at opengl render to texture tutorials, but tbh I wouldn't recommend it, the technique does slow things down and not all 3d cards can manage > 256x256 textures...


FlameDuck(Posted 2007) [#9]
but I want to see if this (probably simpler) method will work first.
It won't. Why? Because you're editing too large an amount of memory to do it effectively.


ImaginaryHuman(Posted 2007) [#10]
If texture size is a problem just break the process down into several steps with smaller textures.