hi - I made a short program to plot the Mandelbrot set. My algorithm takes several seconds to draw the set. It iterates over every ‘pixel’ in my 320 x 480 screen and calculates what shade of grey it should be, then creates a 1 x 1 rectangle with that shade of grey. display.newRect() is called around 154,000 times. I’m wondering if there’s a faster way to draw the set. Thanks.
Here’s the code.
local MAX\_ITER = 80 local re\_start= -2 local re\_end = 1 local im\_start = -1 local im\_end = 1 local WIDTH = display.contentWidth local HEIGHT = display.contentHeight --count and return number of iterations of f(z) = z^2 + c, where c is a complex number (cr + ci), --until either MAX\_ITER is reached or z exceeds 2. local function mandelbrot(cr,ci) local z = 0 local zr = 0 local zi = 0 local zrp = 0 local zip = 0 local n = 0 while math.abs(z) \<= 2 and n \< MAX\_ITER do zr = (zrp \* zrp) + cr - (zip \* zip) zi = (2 \* (zrp \* zip)) + ci z = math.sqrt((zr \* zr) + (zi \* zi)) zrp = zr zip = zi n = n + 1 end return n end draw = function() local t = 0 local real = 0 local im = 0 local mag = 0 local col = 0 for x = 0, WIDTH do for y = 0, HEIGHT do real = re\_start+ ((x/WIDTH) \* (re\_end - RE\_START)) im = IM\_START + ((y/HEIGHT) \* (IM\_END - IM\_START) ) t = mandelbrot(real,im) col = 255 - (t \* 255 / MAX\_ITER) local dot = display.newRect(x,y,1,1) dot:setFillColor(col/255,col/255,col/255) end end end draw()
At 154,000 iterations you’re grinding through a LOT of accesses to the global math followed by a lookup of its abs and sqrt keys. These will add up. For that matter, you don’t even need the absolute value, since you’re adding two squares, and you’re only doing a check, so even the root is unnecessary. Rather, you’d have something like:
repeat zr = (zrp \* zrp) + cr - (zip \* zip) zi = (2 \* (zrp \* zip)) + ci z = (zr \* zr) + (zi \* zi) zrp = zr zip = zi n = n + 1 until z \>= (2 \* 2) or n == MAX\_ITER
There is also some needless recomputation of coefficients and pointless conversion to and from integer colors:
local width\_coeff = (re\_end - re\_start) / WIDTH local height\_coeff = (im\_end - im\_start) / HEIGHT local t\_coeff = 1 / MAX\_ITER -- ^^ outside of draw() for x = 0, WIDTH do real = re\_start + x \* width\_coeff for y = 0, HEIGHT do im = im\_start + y \* height\_coeff t = mandelbrot(real,im) col = 1 - t \* t\_coeff local dot = display.newRect(x,y,1,1) dot:setFillColor(col, col, col) end end
I agree with sporkfin that memory bitmaps are a good fit here.
You might also try to break up draw() with a timer, just because of the sheer number of pixels.
OK I’ve implemented the suggested changes and it is indeed much faster. It renders the set almost immediately now. However, I’m also try to implement a zoom feature, where you tap a point on the set, and it zooms in on that point x 10. This zoom seems to work, but again, it’s tremendously slow (10 or 15 seconds to render the zoom. I’m not sure why, since it’s using the same draw function as the quick first iteration, and I can tell from print statements that it’s making the calculation of the new start and end points immediately. Here’s my new code, plus the zoom listener function.
Can’t test it myself right now, but that tex:invalidate() call should for sure only happen once after you’ve updated all the pixels and not for each pixel (I’d guess that, at the moment, you’re doing 154000 full texture updates in your draw function).
Yeah, seems to be a very suboptimal code sample as the only reason for an explicit invalidate() call is to be able to delay the work it’s doing until all pixels have been modified.