Monday, January 02, 2012

Turbulence in HTML5 Canvas

This is a screenshot of the shader UI showing what basic turbulence looks like.
As an update to my earlier post on procedural texturing in HTML5 canvas, I wanted to post an improved (faster) version of the earlier code that also incorporates a turbulence function, which is probably more properly called fractal noise.

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!