Flash8 Perlin Marble Texture

Tags:

Last week, I wrote an article about using Flash 8 Perlin noise to generate a wood texture. Today, it's marble.

According to this link, the Perlin formula for marble is:

texture = cosine( x + perlin(x,y,z) )

Again, we don't want to do math on every pixel -- too slow. So, how do we do this with the existing Flash 8 API? First, let's pretend that the perlin term in the above equation is not there. Here's what our image would look like (this image is arbitrarily blue monochrome):

Sinusoid Base for Marble

One pixel scan line of the above image was generated with Math.cos() and BitmapData.setPixel(). The bitmap was then stretched vertially -- no need to call setPixel on w * h pixels -- w x 1 is enough.

The perlin term randomly distorts the phase. In the context of the above image, this means pixels are randomly displaced horizontally. A DisplacementMapFilter driven by perlin is exactly what we need:
Marble, Not-Color Corrected

Then we can map the colors to realistic marble colors using ColorMatrixFilter:
Marble, Corrected

I find the sinusoid basis for the marble to be too regular. We can use perlin noise instead of a sinusoid and our pre-DisplacementMapFilter image looks like this:
Perlin base marble

You can play with the settings here. BTW, I find it takes a lot of trial-and-error to find parameters that result in a decent marble.

Replacement image.

Here's the code:

/**
 * @author jmay
 * www.connectedpixel.com 
 * All original source code listed here is licensed under a Creative Commons License. 
*/
import flash.display.BitmapData;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.geom.Matrix;
import flash.filters.BlurFilter;
import flash.filters.BitmapFilter;
import flash.filters.ColorMatrixFilter;
import flash.filters.DisplacementMapFilter;
import flash.geom.ColorTransform;
 
 
class com.connectedpixel.texture.Marble {
    
    ////////////////////////////////////////////////////////////
    // Properties.  Settable via setter/getters below
    
    // Sinusoidal or Perlin floor
    private var _bSinusoidalFloor:Boolean = false;
    
    // Sinusoidal floor parameters
    private var _waveLength:Number = 100;
    private var _sinMidColorPt:Number = 128;
    private var _sinContrastMultiplier:Number = 1.0;
        
    // Perlin floor parameters
    private var _bPerlinFloorBaseX:Number = 50;
    private var _bPerlinFloorBaseY:Number = 150;
    private var _floorRandomSeed:Number = 317;
    private var _perlinMidColorPt:Number = 128;
    private var _perlinContrastMultiplier:Number = 2.0;
    
    // Perlin floor modifiers
    private var _contrastMultiplier:Number = 1.0;
    private var _colorMidLevel:Number = 128;  // offset
    private var _veinAngleDeg:Number = 0; 
    
    // Marble Distortion parameters
    private var _bEnableDistortion:Boolean = true;
    private var _baseX          :Number = 20;
    private var _baseY          :Number = 20;    
    private var _nOctaves       :Number = 2; 
    private var _randomSeed     :Number = 147;
    private var _bFractalNoise  :Boolean = false;
    private var _blurX          :Number = 5;
    private var _blurY          :Number = 5;
    
    // Distortion strength
    private var _displacementScaleX = 150;
        
    // Color mapping
    private var _rgb0:Number = 0x1c2a1f;
    private var _rgb1:Number = 0xadc8b4;
    
    ////////////////////////////////////////////////////////////
    
    private var _identityMatrix:Matrix;
    private var _identityColorTrans:ColorTransform;
    
    private var _marbleColorFilter:ColorMatrixFilter;
    
    private function invalidateMarbleColorFilter():Void
    {
        delete _marbleColorFilter;
        _marbleColorFilter = undefined;    
    }    
    
    //////////////////////////////////////////////////////////////////////
    // Properties.
    
    public function set sinusoidalFloor(bSin:Boolean):Void { _bSinusoidalFloor = bSin; }
    public function get sinusoidalFloor():Boolean          { return _bSinusoidalFloor; }
    
    public function set waveLength(len:Number):Void { 
        if (isNaN(len)) return;
        if (len < 2) len = 2;
        if (len > 1000) len = 1000;
        _waveLength = len; 
    }
    public function get waveLength():Number { return _waveLength; }    
    
    public function set sinMidColorPt(val:Number):Void { 
        if (isNaN(val)) return;
        if (val < 10) val = 10;
        if (val > 246) val = 246;
        _sinMidColorPt = val; 
    }
    public function get sinMidColorPt():Number { return _sinMidColorPt; }    
    
    public function set sinContrast(contrast:Number):Void 
    { 
        if (contrast < 0.1 ) contrast = 0.1;
        if (contrast > 10.0) contrast = 10.0;
        _sinContrastMultiplier = contrast; 
    }
    public function get sinContrast():Number { return _sinContrastMultiplier; }
    
    ///////////////////////////////////////////////////////////////////////
    
    public function set perlinMidColorPt(val:Number):Void { 
        if (isNaN(val)) return;
        if (val < 10) val = 10;
        if (val > 246) val = 246;
        _perlinMidColorPt = val; 
    }
    public function get perlinMidColorPt():Number { return _perlinMidColorPt; }    
    
    public function set perlinContrast(contrast:Number):Void 
    { 
        if (contrast < 0.1 ) contrast = 0.1;
        if (contrast > 10.0) contrast = 10.0;
        _perlinContrastMultiplier = contrast; 
    }
    public function get perlinContrast():Number { return _perlinContrastMultiplier; }
    
    public function set floorBaseX(bx:Number):Void { _bPerlinFloorBaseX = bx; }
    public function set floorBaseY(by:Number):Void { _bPerlinFloorBaseY = by; }
    
    public function get floorBaseX():Number { return _bPerlinFloorBaseX; }
    public function get floorBaseY():Number { return _bPerlinFloorBaseY; }
    
    public function set floorRandomSeed(seed:Number):Void 
                                                       { _floorRandomSeed = seed; } 
    public function get floorRandomSeed():Number { return _floorRandomSeed; } 
    
    ////////////////////////////////////////////////////////////////////////////
    public function set enableDistortion(bEnab:Boolean):Void 
                                                 { _bEnableDistortion = bEnab; }
    public function get enableDistortion():Boolean          
                                                    { return _bEnableDistortion; }
    
    public function set perlinBaseX(bx:Number):Void { 
        if (isNaN(bx)) return;
        if (bx < 3) bx = 3;
        if (bx > 1000) bx = 1000;
        _baseX = bx; 
    }
    
    public function get perlinBaseX():Number { return _baseX; }
    
    public function set perlinBaseY(by:Number):Void { 
        if (isNaN(by)) return;
        if (by < 3) by = 3;
        if (by > 1000) by = 1000;
        _baseY = by; 
    }
    public function get perlinBaseY():Number { return _baseY; }
     
    public function set octaves(nOct:Number):Void { _nOctaves = nOct; }
    public function get octaves():Number { return _nOctaves; }
    
    public function set rgb0(rgb:Number):Void { 
        _rgb0 = rgb; invalidateMarbleColorFilter();
    }
    public function get rgb0():Number { return _rgb0; }
    
    public function set rgb1(rgb:Number):Void { 
        _rgb1 = rgb; invalidateMarbleColorFilter();
    }
    public function get rgb1():Number { return _rgb1; }
    
    public function set seed(s:Number):Void { _randomSeed = s; }
    public function get seed():Number { return _randomSeed; }
    
    public function set fractalNoise(bFractal:Boolean):Void { _bFractalNoise = bFractal; }
    public function get fractalNoise():Boolean { return _bFractalNoise; }
    
    public function set displacementScaleX(dx:Number):Void { 
        if (isNaN(dx)) return;
        if (dx < 1) dx = 1;
        if (dx > 256) dx = 256;
        _displacementScaleX = dx; 
    }
    public function get displacementScaleX():Number { return _displacementScaleX; }
    
    ////////////////////////////////////////////////////////////////////////
    
    public function Marble()
    {
        _identityMatrix        = new Matrix();
        _identityColorTrans = new ColorTransform();
    }
    
    ////////////////////////////////////////////////////////////////////////
    // Convenience function.  Returns a bitmap of the desired
    // size using the current Marble settings.
    ///////////////////////////////////////////////////////////////////////
    
    public function createBitmap(w:Number,h:Number):BitmapData
    {
        var Marble_bmp:BitmapData = new BitmapData(w, h, false, 0x000000);
        render(Marble_bmp);
        return Marble_bmp;
    }
    
    /////////////////////////////////////////////////////////
    //  Render the Marble grain onto the bitmap using the 
    //  current property values.  
    //  buffer0_bmp and buffer1_bmp are optional.  If they 
    //  are not suppliedtemporary bitmaps will be created.  
    //  They MUST have the same width and height as 
    //  the destination bmp.
    ////////////////////////////////////////////////////////
    
    public function render(bmp:BitmapData):Void
    {
        var w:Number = bmp.width;
        var h:Number = bmp.height;
        
        // Needed in some of the following flash api calls.        
        var rect:Rectangle = new Rectangle(0,0,w,h);    
        var origin:Point = new Point(0,0);
        
        var mid:Number;
        var mult:Number;
        
        // The source bitmap needs to be larger than the destination bitmap because
        // DisplacementMapFilter will grab pixels from a larger area.
        
        var paddingX:Number = _displacementScaleX + 8; // Add 16 as slop
        
        var floor_bmp:BitmapData = new BitmapData(w+paddingX,h,false,0x000000);
        if (_bSinusoidalFloor){
            drawSine(floor_bmp, _waveLength);
            
            mid = _sinMidColorPt;
            mult = _sinContrastMultiplier;
        }
        else{
            floor_bmp.perlinNoise(_bPerlinFloorBaseX,_bPerlinFloorBaseY,1,
                                             _floorRandomSeed,false,true,4,false);
            mult = _perlinContrastMultiplier;
            mid = _perlinMidColorPt;
        }
        var offset:Number = mult * (mid-128);
        var ampColor:Array = [1, 0, 0, 0,   0,
                              0, 1, 0, 0,   0,
                              0, 0, mult, 0, offset,
                              0, 0, 0, 1,   0 ];
                               
        var ampColorFilter:ColorMatrixFilter = new ColorMatrixFilter(ampColor);
        floor_bmp.applyFilter(floor_bmp,floor_bmp.rectangle, origin, ampColorFilter);        
        
        /////////////////////////////////////////////////////////
        // Add the marble distortion.
        if (_bEnableDistortion){
            // Will hold perlin noise.
            var srcNoise_bmp:BitmapData = 
                              new BitmapData(w+paddingX, h, false, 0xffffffff);
        
            // channelOptions - 4 - blue only
            // grayscale - false
            srcNoise_bmp.perlinNoise(_baseX,_baseY,_nOctaves,_randomSeed,
                                                  false,_bFractalNoise,4,false);
        
            var filter:DisplacementMapFilter  = new 
                     DisplacementMapFilter(srcNoise_bmp,origin,4,1,
                                                      _displacementScaleX,0);
        
            var r:Rectangle = new Rectangle(paddingX/2,0,w,h);
            bmp.applyFilter(floor_bmp,r,new Point(0,0),filter);
        
            srcNoise_bmp.dispose();
        }
        else{
            bmp.copyPixels(floor_bmp,rect,origin);	
        }
        floor_bmp.dispose(); 
        
         // Change it from black and blue to the desired colors.
        bmp.applyFilter(bmp,rect, origin, getMarbleColorFilter());        
    }    
    
    private function drawSine(floor_bmp:BitmapData, wavelength:Number):Void
    {
        var w:Number = floor_bmp.width;
        
        // 1-pixel high bitmap.
        var tmp_bmp:BitmapData = new BitmapData(w, 1);
            
        var phaseInc:Number = 2 * Math.PI / wavelength;
        var phase:Number = 0;
        
        for (var x:Number = 0 ; x < w ; x++){
            phase += phaseInc;
            var colVal:Number = 127 * Math.cos(phase) + 128;
            colVal = Math.round(colVal);
            // We're dealing with blue only here.
            tmp_bmp.setPixel(x,0,colVal);
        }
        
        var stretchMatrix:Matrix = new Matrix();
        stretchMatrix.scale(1,floor_bmp.height);
        
        // Now, draw the movieclip onto the floor_bmp
        var blend:Object = 1; // normal
         floor_bmp.draw(tmp_bmp,stretchMatrix,_identityColorTrans, blend);
            
        // Clean up.
        tmp_bmp.dispose();
    }
    
    ///////////////////////////////////////////////////////////////////////
    // Map the black to blue colors to the desired Marble colors.
    ///////////////////////////////////////////////////////////////////////
    
    private function getMarbleColorFilter():ColorMatrixFilter
    {
        if (_marbleColorFilter != undefined){
            return _marbleColorFilter; 
        }    
        // Apply the desired colors to the bitmap.
        var r0:Number = (_rgb0 >> 16) & 0xff;
        var g0:Number = (_rgb0 >> 8 ) & 0xff;
        var b0:Number = _rgb0 & 0xff;
        var r1:Number = (_rgb1 >> 16) & 0xff;
        var g1:Number = (_rgb1 >> 8 ) & 0xff;
        var b1:Number = _rgb1 & 0xff;
        
        var marbleColor:Array = [0, 0, (r1-r0)/255, 0, r0,
                                 0, 0, (g1-g0)/255, 0, g0,
                                 0, 0, (b1-b0)/255, 0, b0,
                                 0, 0, 0, 1,    0 ];
                               
        _marbleColorFilter= new ColorMatrixFilter(marbleColor);
        
        return _marbleColorFilter;
    }    
    
}

Here are a few generated marbles.

Replacement image.