This is a screenshot of the shader UI showing what basic turbulence looks like. |
First, hat's off to Ryan Sturgell for pointing out that by moving the big array initialization outside of the this.noise = function() call, it's possible to speed up the Perlin noise() function several-fold. Which should have been obvious, but I missed it on the first go-round. #doh
In any case, the noise() function in the improved code (below) now can process ~400K pixels per second, which is a significant improvement indeed.
Turbulence
The turbulence function adds Perlin noise of various frequencies together, like so:
function turbulence( x,y,z, octaves ) { var t = 0; var f = 1; var n = 0; for (var i = 0; i < octaves; i++, f *= 2) { n += PerlinNoise.noise(x * f, y * f, z)/f; t += 1/f; } return n / t; // rescale back to 0..1 }
This has the net result of giving noise that is much more realistic for things like cloud textures or smoke. For example:
x /= w; y /= h; size = 5; y = 1 - bias(y,.4); n = turbulence(size*x,1.8*size*y,1-y,3); y = Math.sqrt(y); r = bias(y,.68) * n * 255; g = r/1.22;
This code uses turbulence to generate the cloud pattern and bias() to stretch the sky a bit at the top (and stretch the red value vertically as well).
Without further ado, here's the complete code for the shader page. Copy and paste all of the following code into a text file and give it a name that ends with .html. Then open it in Chrome, Firefox, or any HTML5-capable browser.
<html> <head> <script> // A canvas demo by Kas Thomas. // http://asserttrue.blogspot.com // Use as you will, at your own risk. context = null; canvas = null; window.onload = function(){ canvas = document.getElementById("myCanvas"); canvas.addEventListener('mousemove', handleMousemove, false); context = canvas.getContext("2d"); loadHiddenText(); } function loadHiddenText( ) { var options = document.getElementsByTagName( "option" ); var spans = document.getElementsByTagName( "span" ); for (var i = 0; i < options.length; i++) options[i].value = spans[i].innerHTML; } // should probably be called resetCanvas() function clearImage( ) { canvas.width = canvas.width; } function drawViaCallback( ) { var w = canvas.width; var h = canvas.height; var canvasData = context.getImageData(0,0,w,h); for (var idx, x = 0; x < w; x++) { for (var y = 0; y < h; y++) { // Index of the pixel in the array idx = (x + y * w) * 4; // The RGB values var r = canvasData.data[idx + 0]; var g = canvasData.data[idx + 1]; var b = canvasData.data[idx + 2]; var pixel = callback( [r,g,b], x,y,w,h); canvasData.data[idx + 0] = pixel[0]; canvasData.data[idx + 1] = pixel[1]; canvasData.data[idx + 2] = pixel[2]; } } context.putImageData( canvasData, 0,0 ); } function fillCanvas( color ) { context.fillStyle = color; context.fillRect(0,0,canvas.width,canvas.height); } function doPixelLoop() { var code = document.getElementById("code").value; var f = "callback = function( pixel,x,y,w,h )" + " { var r=pixel[0];var g=pixel[1]; var b=pixel[2];" + code + " return [r,g,b]; }"; try { eval(f); fillCanvas( "#FFFFFF" ); drawViaCallback( ); } catch(e) { alert("Error: " + e.toString()); } } function handleMousemove (ev) { var x, y; // Get the mouse position relative to the canvas element. if (ev.layerX || ev.layerX == 0) { // Firefox x = ev.layerX; y = ev.layerY; } else if (ev.offsetX || ev.offsetX == 0) { // Opera x = ev.offsetX; y = ev.offsetY; } document.getElementById("myCanvas").title = x + ", " + y; } // This is a port of Ken Perlin's Java code. PerlinNoise = new function() { var p = new Array(512) var permutation = [ 151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 ]; for (var i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; this.noise = function(x, y, z) { var X = Math.floor(x) & 255, // FIND UNIT CUBE THAT Y = Math.floor(y) & 255, // CONTAINS POINT. Z = Math.floor(z) & 255; x -= Math.floor(x); // FIND RELATIVE X,Y,Z y -= Math.floor(y); // OF POINT IN CUBE. z -= Math.floor(z); var u = fade(x), // COMPUTE FADE CURVES v = fade(y), // FOR EACH OF X,Y,Z. w = fade(z); var A = p[X ]+Y, AA = p[A]+Z, AB = p[A+1]+Z, // HASH COORDINATES OF B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z; // THE 8 CUBE CORNERS, return scale(lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD grad(p[BA ], x-1, y , z )), // BLENDED lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS grad(p[BB ], x-1, y-1, z ))),// FROM 8 lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS grad(p[BA+1], x-1, y , z-1 )), // OF CUBE lerp(u, grad(p[AB+1], x , y-1, z-1 ), grad(p[BB+1], x-1, y-1, z-1 ))))); } function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp( t, a, b) { return a + t * (b - a); } function grad(hash, x, y, z) { var h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE var u = h<8 ? x : y, // INTO 12 GRADIENT DIRECTIONS. v = h<4 ? y : h==12||h==14 ? x : z; return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v); } function scale(n) { return (1 + n)/2; } } function turbulence( x,y,z, octaves ) { var t = 0; var f = 1; var n = 0; for (var i = 0; i < octaves; i++, f *= 2) { n += PerlinNoise.noise(x * f, y * f, z)/f; t += 1/f; } return n / t; // rescale back to 0..1 } // Perlin's bias function function bias( a, b) { return Math.pow(a, Math.log(b) / Math.log(0.5)); } </script> </head> <body> <canvas id="myCanvas" width="300" height="300"> </canvas><br/> <input type="button" value=" Erase " onclick="clearImage(); "/> <select onchange= "document.getElementById('code').innerHTML = this.value;"> <option>Choose something, then click Execute</option> <option>Basic Perlin Noise</option> <option>Basic Turbulence</option> <option>Waterfall</option> <option>Spherical Nebula</option> <option>Green Fibre Bundle</option> <option>Orange-Blue Marble</option> <option>Blood Maze</option> <option>Yellow Lightning</option> <option>Downward Rainbow Wipe</option> <option>Noisy Rainbow</option> <option>Burning Cross</option> <option>Fair Skies</option> </select> <br/> <textarea id="code" type="textarea" cols="37" rows="7">/* Enter code here. Globals: r,g,b,x,y,w,h,PerlinNoise.noise(a,b,c) */</textarea> <br/> <input type="button" value=" Execute " onclick="doPixelLoop();" /> <input type="button" value="Open as PNG" onclick="window.open(canvas.toDataURL('image/png'))"/> <!-- BEGIN HIDDEN TEXT --> <div hidden="true"> <span> // you can enter your own code here! </span> <span> x /= w; y /= h; size = 10; n = PerlinNoise.noise(size*x,size*y,.8); r = g = b = 255 * n; </span> <span> x /= w; y /= h; size = 10; n = turbulence(size*x,size*y,.8,4); r = g = b = 255 * n; </span> <span> x/= 30; y/=3 * (y+x)/w; n = PerlinNoise.noise(x,y,.18); b = Math.round(255*n); g = b - 255; r = 0; </span> <span> centerx = w/2; centery = h/2; dx = x - centerx; dy = y - centery; dist = (dx*dx + dy*dy)/6000; n = PerlinNoise.noise(x/5,y/5,.18); r = 255 - dist*Math.round(255*n); g = r - 255; b = 0; </span> <span> x/=w;y/=h;sizex=3;sizey=66; n=PerlinNoise.noise(sizex*x,sizey*y,.1); x=(1+Math.sin(3.14*x))/2; y=(1+Math.sin(n*8*y))/2; b=n*y*x*255; r = y*b; g=y*255; </span> <span> centerx = w/2; centery = h/2; dx = x - centerx; dy = y - centery; dist = 1.2*Math.sqrt(dx*dx + dy*dy); n = PerlinNoise.noise(x/30,y/110,.28); dterm = (dist/88)*Math.round(255*n); r = dist < 150 ? dterm : 255; b = dist < 150 ? 255-r : 255; g = dist < 151 ? dterm/1.5 : 255; </span> <span> n = PerlinNoise.noise(x/45,y/120, .74); n = Math.cos( n * 85); r = Math.round(n * 255); b = 255 - r; g = r - 255 ; </span> <span> x /= w; y /= h; sizex = 1.5; sizey=10; n=PerlinNoise.noise(sizex*x,sizey*y,.4); x = (1+Math.cos(n+2*Math.PI*x-.5)); x = Math.sqrt(x); y *= y; r= 255-x*255; g=255-n*x*255; b=y*255; </span> <span> // This uses no Perlin noise. x/=w; y/=h; b = 255 - y*255*(1 + Math.sin(6.3*x))/2; g = 255 - y*255*(1 + Math.cos(6.3*x))/2; r = 255 - y*255*(1 - Math.sin(6.3*x))/2; </span> <span> x/=w;y/=h; size = 20; n = PerlinNoise.noise(size*x,size*y,.9); b = 255 - 255*(1+Math.sin(n+6.3*x))/2; g = 255 - 255*(1+Math.cos(n+6.3*x))/2; r = 255 - 255*(1-Math.sin(n+6.3*x))/2; </span> <span> x /= w; y /= h; size = 19; n = PerlinNoise.noise(size*x,size*y,.9); x = (1+Math.cos(n+2*Math.PI*x-.5)); y = (1+Math.cos(2*Math.PI*y)); //x = Math.sqrt(x); y = Math.sqrt(y); r= 255-y*x*n*255; g = r;b=255-r; </span> <span> x /= w; y /= h; size = 5; y = 1 - bias(y,.4); n = turbulence(size*x,1.8*size*y,1-y,3); y = Math.sqrt(y); r = bias(y,.68) * n * 255; g = r/1.22; b = 255 - r/2; </span> </div> <!-- END HIDDEN TEXT --> </body> </html>
Incidentally, I did find a halfway-decent discussion of noise() and turbulence() online, written by Ken Perlin himself, at http://http.developer.nvidia.com/GPUGems/gpugems_ch05.html. Read it and reap!