need to rotate a pixmap with smooth filtering

BlitzMax Forums/BlitzMax Programming/need to rotate a pixmap with smooth filtering

Robert Cummings(Posted 2006) [#1]
...in software :)

I know I post a lot but its not as if I won't share the results when I see someone else ask!

So is there a blindingly fast way to rotate stuff like in Blitz+ for the purposes of pre-calculating frames? I want to do a rotation of a 128x128 image 64 times (with alpha channel) and stick it down on a pixmap and save it. I know its big but this will be for software rendering, I think 4 meg isn't much really system ram wise...

Let me know if there is a fast and filtered way of doing it in pixmaps with alpha - thanks!


Grey Alien(Posted 2006) [#2]
sounds pretty complex with alpha, good luck!


Robert Cummings(Posted 2006) [#3]
Thanks! Yep It will be for pre-rendering a list of frames, not for in-game use.


TartanTangerine (was Indiepath)(Posted 2006) [#4]
I've got some c++ code to do this, in fact it'll do it pretty well in real-time. Remind me tomorrow and I'll dig it out for you (I'm off to bed now).


Robert Cummings(Posted 2006) [#5]
Hey thanks Tim - much appreciated! Will this work with bmax?


TartanTangerine (was Indiepath)(Posted 2006) [#6]
It won't work straight off the Bat but with a few tweaks it will.


ImaginaryHuman(Posted 2006) [#7]
It's probably faster to upload your image as an Image once, let Max2D do the smooth filtering and rendering and rotating and alphablending for you, which is done in hardware, and use Grab Pixmap for each frame.


ImaginaryHuman(Posted 2006) [#8]
Below is a BlitzBasic 2 (Amiga) sourcecode to do rotation using my old Mildred graphics library for reading and writing pixels. You might be able to adapt it. Note that most of it is stuff you can cut out, the actual rotate itself is fairly small. Also notice how much more complicated BlitzBasic USED to be ;-D

Commands starting with M are 3rd party Mildred library commands - let me know if you have questions as to what they do. It shouldn't mess up with what you want to do.

".q" types were `quick` types, a 16:16 fixed point format for decimal numbers. You could just use Floats. I'm not sure if BlitzMax has `Pi` .. it's just Pi.. you know... 3.14 etc.

MPlot plots a pixel (like Plot or WritePixel). MPoint reads a pixel (like ... oh, Max doesn't have that, lol ... ReadPixel()?)

This routine is the type where it draws all pixels within a rectangle of destination screen space, regardless of whether some parts of the rotated image are off-screen or outside the rectangle. I think that is what you'll want to use considering you're rotating within a pixmap. If not put a blank border around your source image and make your pixmap bigger to accomodate.

This is also a rotozoomer - you can zoom in and out as well as rotate. That said, to do antialiasing/smoothing you should probably draw to a rather large pixmap while scaling up quite large, and then scale down the results using a simple filtered resizing technique - filtering, smoothing, averaging, whatever.

To handle alpha just read the alpha value of each pixel that you are reading from (the MPoint() calls), divide it by 256.0 to get in the range 0-1.0, and then multiply your pixel's R G and B components by that amount to scale it.

Good luck.

I still think it would be faster and easier to use:

LoadImage
SetScale
SetAlpha
SetRotation
DrawImage
GrabPixmap
SavePixmapPNG

Plus it'll do the filtering for you with high accuracy and speed - except maybe the edges wouldn't be antialiased?

I commend you on your efforts to do software rendering, something close to my heart having written Mildred back in the day to do that very thing (256-colors chunky routines for BlitzBasic 2 on the Amiga). I never did get around to implementing rotation in the library itself but the below routine was what I was working towards. ... My problem with implementing it on the Amiga? .... trying to do it in assembler, I ran out of registers. ;-)

buf.b=0
its.l=0
cnt.b=0
ang.q=0
degrad.q=Pi/180
mag.q=1
ResetTimer
While Joyb(0)<>3 AND Joyb(1)=0
  If Joyb(0)=1 Then mag+0.01
  If Joyb(0)=2 Then mag-0.01
  ang+1
  If ang>=360 Then ang=-360
  If ang<0 Then ang+360
  y_uadd.q=Cos((ang+90)*degrad)*mag
  y_vadd.q=Sin((ang+90)*degrad)*mag
  x_uadd.q=Cos(ang*degrad)*mag
  x_vadd.q=Sin(ang*degrad)*mag
  xcenter=SMouseX
  ycenter=SMouseY
  y_u.q=-(y_uadd*(opheight/2))-(x_uadd*(opwidth/2))
  y_v.q=-(y_vadd*(opheight/2))-(x_vadd*(opwidth/2))
  For y=0 To opheight-1
    x_u.q=y_u
    x_v.q=y_v
    For x=0 To opwidth-1
      MPlot x,y,MPoint(xcenter+x_u,ycenter+x_v,1)
      x_u+x_uadd
      x_v+x_vadd
    Next x
    y_u+y_uadd
    y_v+y_vadd
  Next y


Taken from this original demo program...



Robert Cummings(Posted 2006) [#9]
WOW! You used amiblitz too? I cut my game-making teeth on that stuff! :)

Thank you for your help Angel and Indie, I will look into all your options... gonna be tricky I think.

I should point out this is for a software render mode so I can't use max2D :)

Amiblitz really was a lot more complicated. Times have moved on haven't they?


FlameDuck(Posted 2006) [#10]
You still have Mildred code lying around? Cool! (And slightly disturbing...)


ImaginaryHuman(Posted 2006) [#11]
Yah, back in the days. I started out on Amos - Game Creator, that thing was a classic but unfortunately quite slow by todays standards and not kept up to date except for Amos `Professional`, then I moved onto BlitzBasic 2. I think they now have some kind of product called AmiBlitz now which is a community effort, or something.

Yeah I have all of the Mildred code, source, demo programs, website, documentations etc. 30,000 lines of assembler code ;-) Funny to see the in-line asm in that program up there, too. That was a neat capability for the orignal Blitz. And yes, very slightly disturbing that I still cherish that precious code archive, even though it's more or less useless now.

I don't think that code above will be too hard to modify. I understand about not using Max2D, I guess it would defeat the purpose. The code probably would almost work as it stands. You just need to add the alpha stuff. I'm thinking though that perhaps ultimately you want the kind of rotate that does things the other way around, where it draws only the pixels that are in the rotated rectangle, ie a source-based shape, rather than a destination-based shape as in the above code. But maybe you can adapt it.

FlameDuck: Hey, I think I have quite a few bits of code that YOU wrote for Mildred Mr Duck ;-) Guilty as charged.

Ahhhhhhhhhh, old times.


ImaginaryHuman(Posted 2006) [#12]
Here is the BlitzMax version of the rotozoomer demo. However, it is not working right. I get what seems to be different colors in X but in Y the same color is repeated down the screen. I cannot figure out where the bug is. Can anyone help?

OEJ: Should be easy enough to adapt this to what you want, once it's working, it handles alphablending, rotation, zoom, a background image, and the rotation angle can be a Float.

Anyone see the bug? (My images are just 640x480 images) I am quite certain it has nothing to do with the alphablending. Even with bare minimum of two lines - WritePixel(ReadPixel()) it is still wrong.

'Software rotation demo

Strict
SetGraphicsDriver(GLMax2DDriver())
Graphics 640,480,0

'Prepare pixmaps
Local BackgroundPixmap:TPixmap=LoadPixmap("Background.jpg")
If PixmapFormat(BackgroundPixmap)<>PF_RGBA8888 Then BackgroundPixmap=ConvertPixmap(BackgroundPixmap,PF_RGBA8888)
Local SourcePixmap:TPixmap=LoadPixmap("RotoZoom.jpg")
If PixmapFormat(SourcePixmap)<>PF_RGBA8888 Then SourcePixmap=ConvertPixmap(SourcePixmap,PF_RGBA8888)
Local DestPixmap:TPixmap=CreatePixmap(640,480,PF_RGBA8888,4)
Local SourcePixelsPerRow=PixmapWidth(SourcePixmap)
Local DestPixelsPerRow=PixmapWidth(DestPixmap)
Local OpWidth:Int=128'PixmapWidth(SourcePixmap) 'Width of operation
Local OpHeight:Int=128'PixmapHeight(SourcePixmap) 'Height of operation
Local OpWidth2:Int=OpWidth/2
Local OpHeight2:Int=OpHeight/2
Local XCenter:Int'=OpWidth/2
Local YCenter:Int'=OpHeight/2

'Dummy alpha values
For Local aa:Int=0 To OpHeight-1
	For Local bb:Int=0 To OpWidth-1
		WritePixel(SourcePixmap,bb,aa,ReadPixel(SourcePixmap,bb,aa)+Max(aa+bb,256)) 'Make alpha to see effect if image doesn't have one
	Next
Next

'Set up variables
Local Angle:Float=0
Local Magnification:Float=1.0
Local Y_UAdd,Y_VAdd,X_UAdd,X_VAdd:Float
Local Y_U,Y_V,X_U,X_V:Float
Local SourceBase:Int Ptr=Int Ptr(PixmapPixelPtr(SourcePixmap,0,0))
Local DestBase:Int Ptr=Int Ptr(PixmapPixelPtr(DestPixmap,0,0))
Local SourcePixel,DestPixel:Int
Local Red,Green,Blue:Int
Local Alpha:Float

'Demo
Repeat
	'Interaction/autorotate
	If MouseDown(1) Then Magnification:+0.01
	If MouseDown(2) Then Magnification:-0.01
	Angle:+1
	If Angle>=360 Then Angle:-360
	If Angle<0 Then Angle:+360

	'Setup
	Y_UAdd=Cos(Angle+90)*Magnification
	Y_VAdd=Sin(Angle+90)*Magnification
	X_UAdd=Cos(Angle)*Magnification
	X_VAdd=Sin(Angle)*Magnification
	XCenter=MouseX()
	YCenter=MouseY()
	Y_U=-(Y_UAdd*OpHeight2)-(X_UAdd*OpWidth2)
	Y_V=-(Y_VAdd*OpHeight2)-(X_VAdd*OpWidth2)
	
	'Draw it
	DestPixmap.Paste(BackgroundPixmap,0,0) 'Draw background
	For Local Y:Int=0 To OpHeight-1
		X_U=Y_U
		X_V=Y_V
		For Local X:Int=0 To OpWidth-1
			SourcePixel=Int Ptr(SourceBase + Min(Max(0,XCenter+X_U),OpWidth) + ((Min(Max(0,YCenter+X_V),OpHeight) * SourcePixelsPerRow)))[0] 'Read full pixel, clipped
			'SourcePixel=ReadPixel(SourcePixmap,XCenter+X_U,YCenter+X_V)
			Alpha=(SourcePixel Shr 24)/256.0
			Red=((SourcePixel Shl 8) Shr 24) * Alpha
			Green=((SourcePixel Shl 16) Shr 24) * Alpha
			Blue=(SourcePixel & $FF) * Alpha
			Alpha=1.0-Alpha
			DestPixel=(DestBase+X+(Y*OpWidth))[0] 'Read full pixel, clipped
			'DestPixel=ReadPixel(DestPixmap,X,Y)
			Red:+(((DestPixel Shl 8) Shr 24) * Alpha)
			Green:+(((DestPixel Shl 16) Shr 24) * Alpha)
			Blue:+((DestPixel & $FF) * Alpha)
			(DestBase+X+(Y*DestPixelsPerRow))[0]=(Red Shl 24) | (Green Shl 16) | (Blue Shl 8) 'Plot RGBA
			'WritePixel(DestPixmap,X,Y,(Red Shl 24)|(Green Shl 16)|(Blue Shl 8)) 'Write RGBA
			'WritePixel DestPixmap,X,Y,SourcePixel
			X_U:+X_UAdd
			X_V:+X_VAdd
		Next
		Y_U:+Y_UAdd
		Y_V:+Y_VAdd
	Next
	DrawPixmap DestPixmap,0,0
	Flip 1
Until KeyHit(KEY_ESCAPE)



ImaginaryHuman(Posted 2006) [#13]
Can anyone tell me why exactly, these lines do not work:

        Y_UAdd=Cos(Angle+90)*Magnification
        Y_VAdd=Sin(Angle+90)*Magnification
        X_UAdd=Cos(Angle)*Magnification
        X_VAdd=Sin(Angle)*Magnification


Even if I take out the *Magnification, it seems that Y_UAdd, Y_VAdd and X_UAdd are all 0's whilst X_Vadd is the only one that has a value.

When I do:
        Print Cos(Angle+90)
        Print Sin(Angle+90)
        Print Cos(Angle)
        Print Sin(Angle)


I see valid numbers for all variables. But if I try to PUT those numbers into a variable they turn into 0's, even if I change the variables to Doubles.

What gives? Is this a bug?


Yan(Posted 2006) [#14]
Oh...You're gonna kick yourself... ;o)
Local Y_UAdd,Y_VAdd,X_UAdd,X_VAdd:Float
Local Y_U,Y_V,X_U,X_V:Float

Should be...
Local Y_UAdd:Float, Y_VAdd:Float, X_UAdd:Float, X_VAdd:Float
Local Y_U:Float, Y_V:Float, X_U:Float, X_V:Float



tonyg(Posted 2006) [#15]
These are declared as Int...
Local Y_UAdd,Y_VAdd,X_UAdd,X_VAdd:Float
except X_VAdd


ImaginaryHuman(Posted 2006) [#16]
Is that strange!

*kicks self, lightly*

I thought you could separate variables by comma's and if you define the type on the last one then it applies to all of them. ????

I guess that might explain why a list of Int's doesn't produce an error but floats do - because the others default to int's?

Well. Will have to change this as soon as I get home and try it out. I presume the demo now works for you, with that modification?

(Thanks Lenny!)


tonyg(Posted 2006) [#17]
I get garbled/corrupt display.


ImaginaryHuman(Posted 2006) [#18]
The next thing to do with this would be to turn the loops inside out, ie the X and Y counters should be set outside the loop and with an X_Add and Y_Add increment, which steps through the source pixmap in non-rotated space, and then the X_U,X_V and Y_U,Y_V gets translated to loop counters in the For-Next loops.

At the moment you're looking at a non-rotated viewport into a rotating world, whereas what you probably want is a rotated viewport into a non-rotated world. .. then you can set the rotation angle and have it draw all of the pixels from the source pixmap, and only those pixels, at the rotated angle like Max2D does. I think that should be simple enough.

Then all that's left to add is filtering. I guess you can't really draw the rotated image to some buffer in a big size and then scale it down as it would be more efficient to somehow filter it on the fly.

I guess it totally depends what kind of filtering you want - what algorithm. Different levels of antialiasing, ie 2x2, 4x4, 8x8 etc? You'd have to probably accumulate the values for all the pixels in the square and divide by total number of pixels (use a Shift!), and then plot that pixel? That would at least be antialiased, but I'm not sure how it'd look when zoomed up big .... not quite the same as bi/trilinear filtering/interpolation. I suppose you could interpolate from the color of one pixel to the next based on what decimal portion of the source pixel you are at.


ImaginaryHuman(Posted 2006) [#19]
Try removing my custom read/writes code and just use the ReadPixel() WritePixel() calls.. see if that works .. .I noticed some kind of difference with that.

When you say garbled is that beacuse I added a part that draws random alpha values to each pixel in the source pixmap?

Do you at least see rotation of the garble?


ImaginaryHuman(Posted 2006) [#20]
One Eyed Jack: I think you might find that doing rotation, with alpha blending, is probably not going to be quite as fast as you might hope for compared to Blitz+ .... The alpha blending itself is kind of expensive and does slow things down. If add filtering in there it's going to get even slower. That said, maybe it's actually pretty fast but it just seems slow due to the DrawPixmap() call which is always slow. Would be interesting to time the loop without the DrawPixmap() or Flip 1.


tonyg(Posted 2006) [#21]
Do you at least see rotation of the garble?

The garble rotates within, what looks like , it's own little viewport.


ImaginaryHuman(Posted 2006) [#22]
Ok good. Perhaps the pixmap is corrupt, or we just need decent alpha values.

I presume it zooms in and out with the mouse buttons?

Yah, being within its own viewport is basically how the routine currently works. It needs to be turned inside out, to have the source counters made into the For-Next loop counters, and put the X and Y as separate counters with add-variables.


Robert Cummings(Posted 2006) [#23]
Hi Angel, thanks for trying. It's a huge challenge I think, and one thats possibly better of being done in C++?


ImaginaryHuman(Posted 2006) [#24]
I don't know, I think it's simple enough. It'd be interesting to see speed comparisons with C++ and BMax.

I'll get the bugs out and see if I can turn it into a one where you take an image and draw the whole thing rotated.

I am just thinking maybe there is a better algorith that does some kind of scanline business on the rotated polygon so as not to be missing pixels or plotting at scattered locations.

[EDIT] Come to think of it that's probably not so difficult as it sounds. You could figure out the edges of the polygon that is to be drawn, sort the rows in Y order, then do one scanline at a time reading data from the source in diagonal lines. Not sure if it'd be more efficient given the extra work needed but who knows.[/EDIT]


ImaginaryHuman(Posted 2006) [#25]
Here is the working rotozoomer. However, I had to remove my own speedier `read` of the source pixmap because in debug mode it works fine, in non-debug mode it crashes. It's something to do with maybe it's reading outside of the pixmap, but I have put a clip in place to prevent that, so I'm not sure what the bug is.

Still ... this version should work.... alphablended rotozoomer. Left mouse button zooms out, right mouse button or space bar zooms in.

'Software rotation demo

Strict
SetGraphicsDriver(GLMax2DDriver())
Graphics 640,480,0

'Prepare pixmaps
Local BackgroundPixmap:TPixmap=LoadPixmap("Background.jpg")
If PixmapFormat(BackgroundPixmap)<>PF_RGBA8888 Then BackgroundPixmap=ConvertPixmap(BackgroundPixmap,PF_RGBA8888)
Local SourcePixmap:TPixmap=LoadPixmap("RotoZoom.jpg")
If PixmapFormat(SourcePixmap)<>PF_RGBA8888 Then SourcePixmap=ConvertPixmap(SourcePixmap,PF_RGBA8888)
Local DestPixmap:TPixmap=CreatePixmap(640,480,PF_RGBA8888,4)
Local SourcePixelsPerRow=PixmapPitch(SourcePixmap)/4
Local DestPixelsPerRow=PixmapPitch(DestPixmap)/4
Local OpWidth:Int=PixmapWidth(SourcePixmap) 'Width of operation
Local OpHeight:Int=PixmapHeight(SourcePixmap) 'Height of operation
Local OpWidth2:Int=OpWidth/2
Local OpHeight2:Int=OpHeight/2
Local XCenter:Int'=OpWidth/2
Local YCenter:Int'=OpHeight/2

'Dummy alpha values
For Local aa:Int=0 To OpHeight-1
	For Local bb:Int=0 To OpWidth-1
		WritePixel(SourcePixmap,bb,aa,(ReadPixel(SourcePixmap,bb,aa)&$00FFFFFF)|(Int(bb*(256.0/OpWidth)) Shl 24)) 'Make alpha to see effect if image doesn't have one
	Next
Next

'Set up variables
Local PixmapWin:TPixmap=CreateStaticPixmap(PixmapPixelPtr(DestPixmap,0,0),OpWidth-1,OpHeight-1,SourcePixelsPerRow*4,PF_RGBA8888)
Local Angle:Float=0
Local Magnification:Float=1.0
Local Y_UAdd:Float,Y_VAdd:Float,X_UAdd:Float,X_VAdd:Float
Local Y_U:Float,Y_V:Float,X_U:Float,X_V:Float
Local SourceBase:Int Ptr=Int Ptr(PixmapPixelPtr(SourcePixmap,0,0))
Local DestBase:Int Ptr
Local SourcePixel:Int,DestPixel:Int
Local Xpos:Int,YPos:Int
Local Red:Int,Green:Int,Blue:Int
Local Alpha:Float
Local AlphaMask:Int=$000000FF

'Demo
Repeat
	'Interaction/autorotate
	If MouseDown(1) Then Magnification:+0.01
	If MouseDown(2) Or KeyHit(KEY_SPACE) Then Magnification:-0.01
	Angle:+1
	If Angle>=360 Then Angle:-360
	If Angle<0 Then Angle:+360

	'Setup
	Y_UAdd=Cos(Angle+90)*Magnification
	Y_VAdd=Sin(Angle+90)*Magnification
	X_UAdd=Cos(Angle)*Magnification
	X_VAdd=Sin(Angle)*Magnification
	XCenter=MouseX()
	YCenter=MouseY()
	Y_U=-(Y_UAdd*OpHeight2)-(X_UAdd*OpWidth2)
	Y_V=-(Y_VAdd*OpHeight2)-(X_VAdd*OpWidth2)
	
	'Draw it
	PixmapWin.Paste(BackgroundPixmap,0,0) 'Draw background
	DestBase=Int Ptr(PixmapPixelPtr(DestPixmap,0,0)) 'reset top left
	For Local Y:Int=0 To OpHeight-1
		X_U=Y_U
		X_V=Y_V
		For Local X:Int=0 To OpWidth-1
			XPos=XCenter+Int(X_U)
			YPos=YCenter+Int(X_V)
			If XPos>=0 And XPos<OpWidth 'clip x
				If YPos>0 And YPos<OpHeight 'clip y
					'SourcePixel=( SourceBase + XPos + (YPos*SourcePixelsPerRow) )[0] 'Read full pixel, clipped
					SourcePixel=ReadPixel(SourcePixmap,Xpos,Ypos)
					SourcePixel=(SourcePixel Shl 8) | (SourcePixel Shr 24) 'Convert ARGB to RGBA if using ReadPixel()
					
					Alpha=(SourcePixel & AlphaMask)/256.0
					Red=(SourcePixel Shr 24) * Alpha
					Green=((SourcePixel Shl 8) Shr 24) * Alpha
					Blue=((SourcePixel Shl 16) Shr 24) * Alpha
					Alpha=1.0-Alpha
					DestPixel=DestBase[X] 'Read full pixel
					Red:+((DestPixel Shr 24) * Alpha)
					Green:+(((DestPixel Shl 8) Shr 24) * Alpha)
					Blue:+(((DestPixel Shl 16) Shr 24) * Alpha)
					DestBase[X]=(Red Shl 24) | (Green Shl 16) | (Blue Shl 8) 'Plot RGBA
				EndIf
			EndIf
			X_U:+X_UAdd
			X_V:+X_VAdd
		Next
		DestBase:+DestPixelsPerRow 'next row
		Y_U:+Y_UAdd
		Y_V:+Y_VAdd
	Next
	DrawPixmap PixmapWin,0,0
	Flip 1
Until KeyHit(KEY_ESCAPE)



xlsior(Posted 2006) [#26]
AngelDaniel: It looks *very* weird on my computer... any chance you can link to your background.jpg and rotozoom.jpg, or at least tell me what dimensions your source images are?

When I try to run it with two of my own images, I see a static copy of the background image in the upper left corner, cut to the size of the rotozoom.jpg image, with a 'blind' effect of malformed horizontally aligned lines super imposed over it... Only every 13th line (?!) shows, its stretched horizontally, and the colors of the rotozoom.jpg are way off as well. (it has ni light blue in it). The lines do animate, but only horizontally... Scroll from the right to the left, and change in appearance on the way.

See here for a sample:



ATI Radeon 9600 Pro


ImaginaryHuman(Posted 2006) [#27]
Use anything up to a 640x480 bitmap for both images in 24-bit color. It should work. I have just used two nature images of that size.

Try with widths that are multiples of 8 or something.


ImaginaryHuman(Posted 2006) [#28]
One Eyed Jack: You don't have to use my code by any means, but I was thinking about the different kinds of rotates and how you might make use of them.

There are two kinds: Either you touch every pixel in the destination viewport horizontally and vertically, and at each pixel lookup the rotated coordinates of a pixel in the source image which you copy over ... sort of a `read from` rotation. Then there is the one where you go through the source image horizontally and vertically and copy each pixel to a rotated destination coordinate, sort of a `write to` operation.

The code above is the first type where all pixels in the viewport are assessed for whether they contain valid source coordinates, and currently only draws those pixels that are within the source image, nothing outside it.

It ocurred to me that a better algorithm would be a scaline algorithm like a polygon fill routine, where you do a whole row at a time. Then it occurred to me that the above routine is very close to that. It is actually drawing horizontal scanlines.

So my idea is, for each object that you want to draw, basically give its own `rotation viewport`, the size of which is a bounding box just large enough to contain all of its rotated pixels (and you can re-size it as it rotates). Then use the above routine to draw that pixmap rotated within that viewport. Because the whole thing fits into the viewport at the rotated angle you'll know that you can see all of it, and because it clips to only draw the source pixels whose coordinates are within the source pixmap you'll know it's only drawing those pixels and not anything outside it. To the end user it will *LOOK LIKE* the same results you'd get from a routine that is the other way around - ie reading source data horizontally/vertically and drawing to rotated coords, except the reading and writing of the destination is horizontally/vertically aligned. I think it actually would be faster this way because you have to READ the dest buffer to do an alpha blend as well as write the final values to it, and these two accesses would be more efficient when you're going across a whole row rather than trying to splatter the screen with diagonal lines which might miss some pixels and leave holes.

Then to draw the next object you just work out the new bounding box which is your new viewport into rotation space, and call the rotation routine again with that viewport.

You might be able to optimize through some kind of math, the skipping of pixels that are outside the source pixmap when it is rotated, ie trace diagonal lines until it collides with the edge of the pixmap without having to do every pixel on the line. But otherwise I think this should be fairly quick.

The reason this demo is slow is mainly because of DrawPixmap() which is slow. I'm not sure how you're going to get around that for a software renderer unless you are opening your own display and getting direct access to video mem.

So anyway, I am not going to work on this any more, gotta get back to my game project. But good luck with it. I think filtering should be an easy addition - store the variables of the previous pixel loop for the next one and then use a combination of those pixels with the current pixel, the ratio of which is the decimal part of the coordinates and the inverse of that. [EDIT] ie, if for example the rotation routine says that the current pixel is read from 27.41,12.13, then you know take the value of that pixel and multiply it by .41, then take the value of the pixel next to it and multiply that by the inverse 1.0-.41=.59, and add them together. Do the same in Y (little bit trickier to code) where you take .13 of the current pixel and .87 of the next pixel (current pixel is the blended pixel you just worked out) and then add those together. All together you then divide by 4 (shr 2) to get the average color for each component. Something like that.

Good luck with it. Or just use some other code ;-D


Robert Cummings(Posted 2006) [#29]
Thanks man, you're a lifesaver. I have already learned so much from this code :) It's not what I expected rotation code to look like.

Thanks for sharing and just how come you're so helpful? :)


ImaginaryHuman(Posted 2006) [#30]
You are welcome. I am helpful because I am generous and I like to be of service and when somebody has a need and I know something that can help I like to share it in a teaching sort of capacity. It makes me feel good. Giving is receiving.

I have learned from the code also, you see, because by trying to teach someone something and share something you also become a student.

Truth be told I was curious to see it work as well because it was sort of where I left off with my Mildred graphics library so I was curious to see it working in Max.

I didn't originally author the code/algorithm, truth be told, ie the actual rotation part. I didn't know how to do it and had to ask some chaps on an old Amiga IRC channel who came up with the basic loops and formulas. I think I now understand it better than I did back then. I'm still a bit miffed that my SourcePixel= line only works in debug mode and not regular mode but ReadPixel() isn't too much slower and works with debug off. Maybe you can fix it.

It's really fairly simple, all you are doing is almost the same thing as a non-rotated copy of all the pixels from one pixmap to another. The only difference is that instead of finding the next pixel right to the right of the current one, you have to go up or down a bit of a slope. From what you know of using Cos() and Sin() to draw a circle, ie X=Radius*Cos(Angle) and Y=Radius*Sin(Angle), all it's doing is using this to change what would be a horizontal read into reading from a pixel that is at a given angle. Of course, to accomodate that you have to have a separate pair of X and Y variables to define the pixel, which is why it has four variables to handle the loop rather than just X and Y. The X loop which proceeds from left to right in the source image, instead of assuming that Y=whatever the Y loop currently is, has to also increment/decrement its own Y counter so that it moves on a diagonal. And the same with the Y loop, to move on a diagonal rather than straight down assuming X=0 you have to add an extra X counter to it. So you end up with an outside loop that has an X and Y counter with an adder value for each, and an inner loop that has the same. It's fairly simple really. The alphablend is pretty basic, just split the color into its components and multiply by alpha and combine with the dest components multiplied by the inverse of that alpha.

Nice thing is you can handle rectangular pixmaps. Sometimes rotozoomers used to only handle powers of 2 and would wrap around. This code could be easily enough adapted to wrap around the source texture so that the entire viewport is always full with repeated copies of the pixmap ... just add something like `XPos:Mod OpWidth` and `YPos:Mod OpHeight` just after they are set within the loop, and you'll get your pixmap repeated to fill the whole viewport.

You can also add an extra multiply to the Alpha component, with a `Fade:Float` value from 0 to 1.0, ie `Alpha:*Fade` after reading the Alpha pixel value and before multiplying the components by it. This will let you fade the source pixmap in and out like a `global alphablend` applied to the whole image that simulataneously alphablends the individual pixels based on their alpha values. But that wouldn't be a direct feature replacement for Max2D as Max2D can't do that.

Another feature you could add is very easily done - have an XMagnification and a YMagnification instead of just one mag for both. If these differ, you can stretch the image horizontally and/or vertically by different amounts at the same time as zooming and rotating. I think Max2D should have that feature, it would be easy enough to add a paramter to SetScale, but alas. :-D

The reason that it is detecting presses of the spacebar is because on the Mac with a one-button mouse you don't have a right mouse button to press ;-D

Let us know how you get on.


ImaginaryHuman(Posted 2006) [#31]
I find it weird, I thought I should mention, that in Max ReadPixel and WritePixel require ARGB values, whereas there is no PF_ARGB8888 format for pixmaps. What's the point? It would be better and more consistent and useful to return RGBA and to write RGBA by default. Otherwise they should provide a PF_ARGB8888 format. Notice how I had to rearrange the pixel format on the fly within the main loop after using ReadPixel() - a timewaster.

In the demo code above I had to one moment be doing RGBA and the next ARGB (see the dummy alpha routine), a bit confusing at first.

I also should explain that I used things like Green:+(((DestPixel Shl 8) Shr 24) * Alpha) with the two shifts, because I think this is faster than using an `and` with a mask. Shifts don't require a read of the mask from memory. Shifting left by 8 chops off the top 8 bits and sets them to 0, then shifting right by 24 moves it into the lowest byte ready for multiplying by alpha. I think two shifts is going to be faster than a an `and` and a shift. The mask would have to be an Int so doing $FF as an immediate value wouldn't ensure it would be included in the opcode at the assembler level.

It might be worth your while moving the "DestPixel=DestBase[X] 'Read full pixel" line up a couple of lines to after the Green= line - just to possibly pipeline things a bit better - it can be reading the dest pixel while you're working on other stuff, rather than having to wait for the pixel to be read before the "Red:+((DestPixel Shr 24) * Alpha)" line has to be executed. Might help, dunno.

You might convert the Floats into Doubles for more accuracy - I notice a few anomalies along edges of source rows/columns where you see a the column shift right, then left for one pixel, then right for one, then back left again which can make the image look `bitty`. But that will probably slow it down.

I also don't know if the instances of DestBase[X] are as fast as using DestBase[0] and modifying the DestBase variable with DestBase:+1 in the loop.

It also might be a little bit faster to put the whole alphablending stuff into a single line as part of the pixel write. That would cut out the saving of values into variables along the way.

Also, the OpWidth and OpHeight don't have to be the size of the source pixmap, they can be smaller. e.g. 128x128. You could also do Flip 0 to gain a little bit ;-D