asimes
|
|
« on: August 27, 2013, 07:23:47 AM » |
|
I made a gray scale Buddhabrot renderer in Processing a while ago and am trying it out in JavaScript / Canvas at the moment. I'm using more or less the same idea to try to pre-calculate initial points, trying to see what to do to speed it up. At the moment I am testing with 1,000 as the maximum iteration and am getting 1,000 random points plotted about every 4 seconds. Here's what I'm doing: 1.) Before anything else, run all pixels through the Mandelbrot Set equation to see if they bail or not (don't hit the maximum number of iterations). All pixels that bail are added to an array named nonMSet. 2.) In the function for picking a random point (x, y): - Randomly pick a pixel (an element) from nonMSet. This is guaranteed to not be in the Mandelbrot Set. - Convert the pixel to the complex plane as (x, y) = (pixel to real, pixel to imaginary). - Add a random amount between -0.5 and 0.5 to both x and y (EDIT: I add this amount before converting to the complex plane, the range -0.5 to 0.5 is in pixel coordinates). Doing this means that (x, y) can take on a larger set of values than just the pixels in nonMSet. There is however, a slight risk in that a randomly chosen pixel was right on the boundary of the Mandelbrot Set and the random deviation pushed it in. 3.) Iterating the random point (x, y): - A temporary buffer named orbitBuffer is made of length w*h where w and h are the width and height in pixels. All elements are initialized as 0. - A for loop is used to count to the maximum iteration. Inside the for loop I do the typical Mandelbrot equation code and bail if z's magnitude is greater than 2.0. I also bail if z's real or imaginary component goes outside the window. As long as the bail does not happen, I keep track of where z jumped around in pixel coordinates with orbitBuffer. 4.) Storing where z jumped around: - If the for loop did not reach the maximum iteration then I add orbitBuffer to my pixel buffer. Otherwise the calculation for iterating the random point was a waste (this only happens when the randomly selected pixel from nonMSet was on the boundary of the Mandelbrot Set and the random deviation pushed it in). Luckily it almost never hits the maximum iteration. 5.) Rendering: - Every time steps 1 - 4 happen 1,000 times I render what is in my pixel buffer. At the moment I am using square root coloring, I am not too worried about speeding that up. Here is the pre-calculation for finding points that do not belong in the Mandelbrot Set: Bbrot.prototype.findMSet = function() { // Find all pixels that do not belong to the Mandelbrot Set var x = this.xmin; for (var i = 0; i < this.pixelLength; i++) { var y = this.ymin; for (var j = 0; j < this.pixelLength; j++) { var zr = x; var zi = y; // Iterate z to determine if it would go to infinity var n = 0; for (; n < this.maxIterations; n++) { var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; // If (i, j) does not belong to the Mandelbrot Set if (sqZr+sqZi > 4.0) { this.nonMSet.push(i+j*this.pixelLength); break; } } y += this.delta; } x += this.delta; } oldDate = new Date(); // TEMPORARY, JUST FOR SEEING HOW LONG THINGS TAKE var self = this; var orbitCounter = 1; canvasInterval = setInterval(function() { self.addOrbitTrap(orbitCounter++); }, 0); } Here is my function that does steps 2 - 5: Bbrot.prototype.addOrbitTrap = function(inCount) { // Select a random pixel that does not belong to the Mandelbrot Set var rIndex = (Math.random()*this.nonMSet.length)|0; // Add a random amount (-0.5, 0.5) to the pixel and translate to the complex plane var x = ((this.nonMSet[rIndex]%this.pixelLength)+Math.random()-0.5)*this.delta+this.xmin; var y = (((this.nonMSet[rIndex]/this.pixelLength)|0)+Math.random()-0.5)*this.delta+this.ymin; var zr = x; var zi = y; // This will store the orbit trap z produces var orbitBuffer = []; for (var i = 0; i < this.sqPL; i++) orbitBuffer[i] = 0; var n = 0; for (; n < this.maxIterations; n++) { // Determine if a pixel translated from z would be within the canvas if (zr > this.xmin && zr < this.xmax && zi > this.ymin && zi < this.ymax) { // Translate z to pixel coordinates var px = ((zr-this.xmin)*this.pDelta)|0; var py = ((zi-this.ymin)*this.pDelta)|0; // Increment the appropriate element as well as its symmetric element orbitBuffer[px+py*this.pixelLength]++; orbitBuffer[px+(this.pixelLength-1-py)*this.pixelLength]++; // Iterate z var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; if (sqZr+sqZi > 4.0) break; } else break; } // If the randomly selected pixel did not belong to the Mandelbrot Set add all orbitBuffer[i] to this.pixelBuffer[i] if (n !== this.maxIterations) { for (var i = 0; i < this.sqPL; i++) { this.pixelBuffer[i] += orbitBuffer[i]; if (this.pixelBuffer[i] > this.maxVal) this.maxVal = this.pixelBuffer[i]; } } if (inCount%1000 === 0) { var sqrtMaxVal = Math.sqrt(this.maxVal); for (var i = 0; i < this.sqPL; i++) { var index = i<<2; var pVal = (255.0*Math.sqrt(this.pixelBuffer[i])/sqrtMaxVal)|0; this.imageData.data[index] = pVal; this.imageData.data[index+1] = pVal; this.imageData.data[index+2] = pVal; } this.canvas.getContext("2d").putImageData(this.imageData, 0, 0); // TEMPORARY, JUST FOR SEEING HOW LONG THINGS TAKE var newDate = new Date(); debug.innerHTML = inCount+", "+(newDate.getTime()-oldDate.getTime()); oldDate = new Date(); } }
|
|
« Last Edit: August 27, 2013, 07:51:43 AM by asimes »
|
Logged
|
|
|
|
asimes
|
|
« Reply #1 on: August 27, 2013, 08:06:09 AM » |
|
After 100,000 loops of addOrbitTrap() I got this: I changed the coloring to: - Red is linear color - Green is square root color - Blue is logarithmic color At 400 x 400 pixels rendering after every 100 loops of addOrbitTrap() at about ~420 milliseconds (~7 minutes). Waaay too slow but starting to look promising at least.
|
|
« Last Edit: August 27, 2013, 08:13:14 AM by asimes »
|
Logged
|
|
|
|
asimes
|
|
« Reply #2 on: August 27, 2013, 09:27:19 AM » |
|
I made a few discoveries, still not where I want to be speed-wise, but I found removing orbitBuffer significantly speeds up the process for larger w*h images (actually I almost not affected by image size now). Turns out that this is faster: 1.) Using the maximum iterations is overkill in many cases. It seems that for high maximum iterations (like 1,000,000) that 1,000 is plenty for a maximum iteration in finding the non-Mandelbrot Set, nonMSet. Upper bounding this step with 1,000 maximum iterations seems to work well. There is a slightly higher chance of a randomly selected pixel with a random deviation to be in the Mandelbrot Set but it still does not happen often. 2.) No Change 3 / 4.) Testing if the randomly selected point with a random deviation is not in the Mandelbrot Set first and then running almost the same code again to increment my pixel buffer directly (instead of using orbitBuffer) is much faster. This kind of makes sense because adding all the elements of orbitBuffer to my pixel buffer is asymptotically Theta(w*h) which is why larger images were so slow for me. The only Theta(w*h) calculation I have now is assigning the highestVal among my pixel buffer's elements which is not that significant. Even though both passes of iterating z are O(maximum iterations), on average this seems to be significantly less than Theta(w*h) for large images. 5.) Only change is that I am using linear, square root, and logarithmic coloring like in the last post. New code for pre-calculating the non-Mandelbrot Set (almost the same): Bbrot.prototype.findMSet = function() { // 1,000 is a reasonable non-Mandelbrot Set for higher maximum iterations var boundMaxIterations = Math.min(this.maxIterations, 1000); // Find all pixels that do not belong to the Mandelbrot Set var x = this.xmin; for (var i = 0; i < this.pixelLength; i++) { var y = this.ymin; for (var j = 0; j < this.pixelLength; j++) { var zr = x; var zi = y; // Iterate z to determine if it would go to infinity var n = 0; for (; n < boundMaxIterations; n++) { var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; // If (i, j) does not belong to the Mandelbrot Set if (sqZr+sqZi > 4.0) { this.nonMSet.push(i+j*this.pixelLength); break; } } y += this.delta; } x += this.delta; } oldDate = new Date(); // TEMPORARY, JUST FOR SEEING HOW LONG THINGS TAKE var self = this; var orbitCounter = 1; canvasInterval = setInterval(function() { self.addOrbitTrap(orbitCounter++); }, 0); } New code for steps 2-5: Bbrot.prototype.addOrbitTrap = function(inCount) { // Select a random pixel that does not belong to the Mandelbrot Set var rIndex = (Math.random()*this.nonMSet.length)|0; // Add a random amount (-0.5, 0.5) to the pixel and translate to the complex plane var x = ((this.nonMSet[rIndex]%this.pixelLength)+Math.random()-0.5)*this.delta+this.xmin; var y = (((this.nonMSet[rIndex]/this.pixelLength)|0)+Math.random()-0.5)*this.delta+this.ymin; // Make sure the random deviation did not push z into the Mandelbrot Set var zr = x; var zi = y; var n = 0; for (; n < this.maxIterations; n++) { var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; if (sqZr+sqZi > 4.0) break; } // If z does not belong to the Mandelbrot Set if (n !== this.maxIterations) { zr = x; zi = y; n = 0; for (; n < this.maxIterations; n++) { // Determine if a pixel translated from z would be within the canvas if (zr > this.xmin && zr < this.xmax && zi > this.ymin && zi < this.ymax) { // Translate z to pixel coordinates var px = ((zr-this.xmin)*this.pDelta)|0; var py = ((zi-this.ymin)*this.pDelta)|0; // Increment the appropriate element as well as its symmetric element this.pixelBuffer[px+py*this.pixelLength]++; this.pixelBuffer[px+(this.pixelLength-1-py)*this.pixelLength]++; // Iterate z var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; if (sqZr+sqZi > 4.0) break; } else break; } } if (inCount%100 === 0) { // Set this.maxVal as the highest valued element in this.pixelBuffer for (var i = 0; i < this.sqPL; i++) this.maxVal = (this.pixelBuffer[i] > this.maxVal) ? this.pixelBuffer[i] : this.maxVal; var sqrtMaxVal = Math.sqrt(this.maxVal); var logMaxVal = Math.log(this.maxVal); for (var i = 0; i < this.sqPL; i++) { // Rotate the Buddhabrot var index = ((i%this.pixelLength)*this.pixelLength+((i/this.pixelLength)|0))<<2; this.imageData.data[index] = (255.0*this.pixelBuffer[i]/this.maxVal)|0; this.imageData.data[index+1] = (255.0*Math.log(this.pixelBuffer[i])/sqrtMaxVal)|0; this.imageData.data[index+2] = (255.0*Math.log(this.pixelBuffer[i])/logMaxVal)|0; } this.canvas.getContext("2d").putImageData(this.imageData, 0, 0); // TEMPORARY, JUST FOR SEEING HOW LONG THINGS TAKE var newDate = new Date(); debug.innerHTML = inCount+", "+this.maxVal+", "+(newDate.getTime()-oldDate.getTime()); oldDate = new Date(); } } New image going at 1,000,000 maximum iterations for a 600 x 600 image with almost the same speed (but a larger image and 100x maximum iterations at ~425 milliseconds per 100 loops of addOrbitTrap()):
|
|
|
Logged
|
|
|
|
asimes
|
|
« Reply #3 on: August 28, 2013, 08:36:07 AM » |
|
Got it down to about ~20-30 milliseconds per 1,000 added orbit traps. The solution was to just not call addOrbitTrap() using setInterval() so often and instead do some specified number of orbit traps in a for loop for steps 2-4 in addOrbitTrap(). Then at the end of the function the rendering happens (after the for loop). I guess that has more to do with JavaScript timing but it is a gigantic speed improvement.
|
|
|
Logged
|
|
|
|
asimes
|
|
« Reply #4 on: August 29, 2013, 07:32:36 AM » |
|
I tried out histogram equalization for each r, g, b channel. The images produced look much better regardless of the number of orbit traps added, but it does take a little bit for them to mix channels nicely. I'll post a link to the Buddhabrot render as soon as I finish up the UI. Red channel max iterations: 1,000,000 Blue channel max iterations: 10,000 Green channel max iterations: 100
|
|
|
Logged
|
|
|
|
cKleinhuis
|
|
« Reply #5 on: August 29, 2013, 07:53:27 AM » |
|
do i see this right ? you are using javascript for this?! the rules of thumb for normal programming languages count here as well,i just read your statement with the function calls ... javascript is an interpreted language, so, every function call slows down your execution time, for managed languages one of the most time critical thing is the "new" operation, or the general work for the garbage collector, try re-using arrays instead of de-referencing them and then creating a blank one this is very expensive, in your inner for loop where you have the calculation of the temporary array, make sure you do not recreate this array for every n-iterations, the same goes for variables defined in the loop, to hardcore optimize the javascript code you should reuse all your inner loop variables as often as humanly possible - coming down to declare them globally without name clashing in the functions as long as you arent using threading there is always just one function active at the same time, and thus with carefully selecting the global names name clashes in the functions shall be easily to counter as a starter you could just move the "var" variable definitions from your inner loops to the beginning of the function, and measure the difference in execution time, if that brought you something continue and make them globally, removing ANY "var" DEFINITION out of your functions/loops!!!
|
|
« Last Edit: August 29, 2013, 08:10:56 AM by cKleinhuis, Reason: editing »
|
Logged
|
---
divide and conquer - iterate and rule - chaos is No random!
|
|
|
asimes
|
|
« Reply #6 on: August 29, 2013, 08:41:22 AM » |
|
Yes, it is JavaScript I could try declaring the temporary variables outside the loop, but all in all it is executing 10,000 loops (i) of Mandelbrot Set loops (n) at the moment and rendering within 33 milliseconds for arbitrary image sizes, so I'm fairly happy (not limited by 33 milliseconds, just timed to do it that often). I agree about the temporary array, that turned out to be slow, I removed it later (a version of the code is in a previous post). It was faster to just do the Mandelbrot Set code twice, once to make sure the random point did not belong to the Mandelbrot Set and then again to increment the pixel buffers (there are three now, for r, g, and b). Here are some more:
|
|
|
Logged
|
|
|
|
cKleinhuis
|
|
« Reply #7 on: August 29, 2013, 08:48:31 AM » |
|
and? have you compared the speed when moving the "var" variable definitions to outside the loop?
|
|
|
Logged
|
---
divide and conquer - iterate and rule - chaos is No random!
|
|
|
hobold
Fractal Bachius
Posts: 573
|
|
« Reply #8 on: August 29, 2013, 12:32:26 PM » |
|
|
|
|
Logged
|
|
|
|
|
asimes
|
|
« Reply #10 on: August 30, 2013, 07:36:34 AM » |
|
cKleinhuls, I tried declaring all of my temporary variables outside the loops. The outer loop is for how many orbit traps to try to add in the function addOrbitTrap(inRGB) (set to 10,000). There are two inner loops, both do the Mandelbrot Set code and loop up to maxIterations (set to 1,000,000 for red, 10,000 for green, and 100 for blue) but the second one actual increments my buffer that gets used for the histogram equalization. The inner loops alternate among red, green, and blue every time addOrbitTraps(inRGB) is called (using inRGB%3). Then the histogram equalization code runs for the current color channel in the same function. For both tests I set the interval for addOrbitTraps(inRGB) to 100 milliseconds (that is how often it is called). I added some code at the end of the function to give the time the function took and the averaged the times until I had a total of ~60,000,000 successful orbit traps added: - For the code declaring temporary variables within the loops the average time was: 45.263978762236604 - For the code declaring the variables before the loops the average time was: 46.90922030681639 Admittedly, this is higher than I thought. I had done the average time for the function before by eyeballing just the milliseconds addOrbitTraps(inRGB) took and printed the values. The values printed so fast that I did not realize every once in a while it spikes to 70-110 which is what made the average higher than I thought. Most of the time it is around ~20 but those spike increase the average to ~45. This is the current code (with temporary values declared in the loops, timing code removed): Bbrot.prototype.addOrbitTrap = function(inRGB) { var bbrotRGB, rgbMod = inRGB%3; if (rgbMod === 0) bbrotRGB = document.getElementById("bbrotROrbits"); else if (rgbMod == 1) bbrotRGB = document.getElementById("bbrotGOrbits"); else bbrotRGB = document.getElementById("bbrotBOrbits"); for (var i = 0; i < 10000; i++) { // Select a random pixel that does not belong to the Mandelbrot Set var rIndex = (Math.random()*this.nonMSet[rgbMod].length)|0; // Add a random amount (-0.5, 0.5) to the pixel and translate to the complex plane var x = ((this.nonMSet[rgbMod][rIndex]%this.pixelLength)+Math.random()-0.5)*this.delta+this.xmin; var y = (((this.nonMSet[rgbMod][rIndex]/this.pixelLength)|0)+Math.random()-0.5)*this.delta+this.ymin; // Make sure the random deviation did not push z into the Mandelbrot Set var zr = x; var zi = y; var n = 0; for (; n < this.maxIterations[rgbMod]; n++) { var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; if (sqZr+sqZi > 4.0) break; } // If z does not belong to the Mandelbrot Set if (n !== this.maxIterations[rgbMod]) { zr = x; zi = y; n = 0; for (; n < this.maxIterations[rgbMod]; n++) { // Determine if a pixel translated from z would be within the canvas if (zr > this.xmin && zr < this.xmax && zi > this.ymin && zi < this.ymax) { // Translate z to pixel coordinates var px = ((zr-this.xmin)*this.pDelta)|0; var py = ((zi-this.ymin)*this.pDelta)|0; // Increment the appropriate element as well as its symmetric element this.rgbBuffers[rgbMod][px+py*this.pixelLength]++; this.rgbBuffers[rgbMod][px+(this.pixelLength-1-py)*this.pixelLength]++; // Iterate z var sqZr = zr*zr; var sqZi = zi*zi; var twoZri = 2.0*zr*zi; zr = sqZr-sqZi+x; zi = twoZri+y; if (sqZr+sqZi > 4.0) break; } else break; } this.orbitTraps[rgbMod]++; } } bbrotRGB.innerHTML = this.orbitTraps[rgbMod]; // Set maxVal as the highest valued element in this.rgbBuffers[rgbMod] var maxVal = 0; for (var i = 0; i < this.sqPL; i++) maxVal = (this.rgbBuffers[rgbMod][i] > maxVal) ? this.rgbBuffers[rgbMod][i] : maxVal; maxVal++; // Needed for the last element in cdf[] and histogram[] // Make a histogram of the values var histogram = []; for (var i = 0; i < maxVal; i++) histogram[i] = 0; for (var i = 0; i < this.sqPL; i++) histogram[this.rgbBuffers[rgbMod][i]]++; // Make a cumulative distribution function of the histogram var minCdf = 0x7fffffff; var cdf = []; cdf[0] = histogram[0]; for (var i = 1; i < maxVal; i++) { cdf[i] = histogram[i]+cdf[i-1]; minCdf = (cdf[i] < minCdf) ? cdf[i] : minCdf; } // Apply histogram equalization to this.rgbBuffers[rgbMod][i]'s elements var hDivisor = this.sqPL-minCdf; for (var i = 0; i < this.sqPL; i++) { // Rotate the Buddhabrot var index = ((i%this.pixelLength)*this.pixelLength+((i/this.pixelLength)|0))<<2; this.imageData.data[index+rgbMod] = ((cdf[this.rgbBuffers[rgbMod][i]]-minCdf)/hDivisor*255)|0; } this.canvas.getContext("2d").putImageData(this.imageData, 0, 0); }
|
|
« Last Edit: August 30, 2013, 07:46:24 AM by asimes »
|
Logged
|
|
|
|
asimes
|
|
« Reply #11 on: August 30, 2013, 07:43:33 AM » |
|
hobold, that is pretty cool. Admittedly I don't think I have the time to learn it right now because classes have started again and I have a few jobs at the moment. I will definitely look into it in the future. Alef, I don't think I understand your suggestion. What I am doing at the moment is treating red, green, and blue as completely separate histogram equalizations. Each one will have some number of value 0s (possibly one usually many) and some number of value 255s (usually only one or very few). My coloring does not use n, it uses a cumulative distribution function on my histograms for each color channel. Here is an explanation: http://en.wikipedia.org/wiki/Histogram_equalization#Small_image
|
|
|
Logged
|
|
|
|
|
ker2x
Fractal Molossus
Posts: 795
|
|
« Reply #13 on: September 01, 2013, 01:31:29 PM » |
|
Hello forum, long time no see ! Buddhabrot is my favorite fractal So, optimizing the buddhabrot. You can't now for sure that something isn't in the mandelbrot set. What you can now for sure is that some points are in the Mandelbrot set. You can do some check : - is this point in the main cardioid ? (there really is a real cardioid included in the set. - is the point inside one of the 2nd order bulb ? Here is some openCL code (it's just C code) : //Check if choosen point is in MSet bool isInMSet( float cr, float ci, const uint maxIter, const float escapeOrbit) { int iter = 0; float zr = 0.0; float zi = 0.0; float ci2 = ci*ci; float temp;
//Quick rejection check if c is in 2nd order period bulb if( (cr+1.0) * (cr+1.0) + ci2 < 0.0625) return true;
//Quick rejection check if c is in main cardioid float q = (cr-0.25)*(cr-0.25) + ci2; if( q*(q+(cr-0.25)) < 0.25*ci2) return true;
// test for the smaller bulb left of the period-2 bulb if (( ((cr+1.309)*(cr+1.309)) + ci*ci) < 0.00345) return true;
// check for the smaller bulbs on top and bottom of the cardioid if ((((cr+0.125)*(cr+0.125)) + (ci-0.744)*(ci-0.744)) < 0.0088) return true; if ((((cr+0.125)*(cr+0.125)) + (ci+0.744)*(ci+0.744)) < 0.0088) return true;
while( (iter < maxIter) && ((zr*zr+zi*zi) < escapeOrbit) ) { temp = zr * zi; zr = zr*zr - zi*zi + cr; zi = temp + temp + ci; iter++; }
if( iter < maxIter) { return false; } else { return true; }
}
The code is compact, openCL prefer bruteforce math instead of accessing some temporary variable that usually avoid to compute many times the same thing. cr and ci are the coordinates of the point (real & imaginary), generated using : float cr = mix(realMin, realMax, rand.x); float ci = mix(imaginaryMin, imaginaryMax, rand.y);
mix is an openCL function : gentype mix ( gentype x, gentype y, gentype a) Returns the linear blend of x and y implemented as: x + (y - x) * a (convert random 0..1 point to the fractal coordinates) Another intersting thing is that you don't have to look for point on the whole screen. REAL, PARAMETER :: xmin = -1.0, xmax = 2.0, ymin = -1.3, ymax =1.3
(now, that's some fortran code) Also, the same code to check for main cardioid and biggest bulbs, but the code is very compact too. Fortran handle complex math natively IF(((ABS(c - CMPLX(-1,0) )) < 0.25) .OR. ((ABS( 1.0 - SQRT(1-(4*c)) )) < 1.0 ) ) THEN (c is a complex number which is the coordinate of the point being tested)
|
|
|
Logged
|
|
|
|
ker2x
Fractal Molossus
Posts: 795
|
|
« Reply #14 on: September 01, 2013, 01:45:53 PM » |
|
Now you have another problem : You test each pixel to now of it's in the mandelbroset or not. Which is wrong. each pixel can possibly hold an infinite numbers of point that are and are not in the mandelbrot set. That's why your buddhabroth is noisy. You probably already now that the intersting points for the buddhabrot are at the frontier of the mandelbrot set. By doing what you do you dismiss an infinity of interesting points. You test the infinitesimal point that is at the center of your pixel. But can you now for sure that the infinity of point/coordinates that are in this pixel are also in the mandelbrot set ? You can't. The best optimisation i know is the metropolis-hasting algorithm http://www.steckles.com/buddha/I helped to write the buddhabrot generation software (C++) : https://github.com/emmmile/buddhaTake a look at this : http://www.fractalforums.com/images-showcase-(rate-my-fractal)/ker2x's-buddhabrot-gallery/You need to run billions and billions of random numbers (much more than the number of pixel) to get something nice and smooth.
|
|
|
Logged
|
|
|
|
|