tinySceneGraph Home Images Contact

Building a better marble

Introduction

Marble is a material that comes in a tremendous number of varieties. This chapters fragment shader aims at simulating a type of bright marble with embedded dark filaments. The shader will generate marble flagstones, suitable to render the floor of a room.

We develop a procedural texture in order to avoid repeating patterns and circumvent resolution issues, immanent to bitmap textures. Since fragment shaders execute only once per pixel (not per sub-sample unless ARB_sample_shading is present), aliasing effects are a serious thread to every procedural shader.

There are a couple of ways to work around aliasing effects, like frequency clamping, analytical integration, super-sampling or analytic prefiltering. The shader developed below trades the loss of some filament details in favour of high frequencies in image space that would cause aliasing. It also uses analytic integration for antialiasing the grid of gaps between the tiles, building on a modified brick shader taken from the OpenGL Shading Language book.

Step 1 - Create dark veins

color calculation based on x-coordinate Basically, the shader will determine whether there is pure white stone or a dark vein a given location based on the model-space vertex coordinate. To start with, we decide to create veins, calculating a fragments color as a sine function of the x-coordinate. This leads to a stripe pattern along the z-direction.
The sine's period takes the tile size into account so that there is one vein per row of tiles:
   // definition of tiles, in meter:
   const vec3 tileSize = vec3(1.1,1.0, 1.1);
   const vec3 tilePct = vec3(0.98,1.0, 0.98);

   vec3 marble_color(float x)
   {
      return vec3 (0.5*x+0.5);
   }

   main ()
   {
      float t = 6.28*vertexPosMC.x / tileSize.x ;
      // replicate over rows of tiles:
      t = sin(t);
      vec3 marbleColor = marble_color(t);
   }

Step 2 - Adjusting filament glow

adjusting filament glow An advantage of deriving the color of a pixel with a sine() function from its location is that the color fades to both sides, rather than resulting in a sharp edge. Nevertheless, the veins produced in step 1 are much to thick and smooth, resulting in a bad falloff/glow. Rather than choosing the pixel color from the x-coordinate directly, we can modify the parameter of marble_color() to have sharper and smaller edges with an exponential falloff. Note that all color channels follow the same curve. This can be modified easily by manipulating individual channels in between the sqrt() calls.
  // Expects -1<x<1
  vec3 marble_color (float x)
  {
    vec3 col;
    x = 0.5*(x+1.);          // transform -1<x<1 to 0<x<1
    x = sqrt(x);             // make x fall of rapidly...
    x = sqrt(x);
    x = sqrt(x);
    col = vec3(.2 + .75*x);  // scale x from 0<x<1 to 0.2<x<0.95
    col.b*=0.95;             // slightly reduce blue component (make color "warmer"):
    return col;
  }

Step 3 - Adding turbulence

Obviously, the veins in step 2 are still much too regular, so lets add some randomness, or noise. As many GLSL drivers do still not support a noise() function, I took a perlin noise implementation from a forum at OpenGL.org, posted by Stefan Gustavsson. This function turned out to work reliably and surprisingly fast.

adding noise adding noise
Added turbulence with amplitude=1.0 Added turbulence with amplitude=8.0

Using 3D noise allows to create pseudo-random values from 3D coordinates, varying in a somewhat predictable way. This is ideal to simulate the 3D nature of a block of material. Polygonal surfaces in 3D space will simply slice through the block.

turbulence() is a function on top of noise that adds noise of multiple frequencies.

Adding turbulence to the input of the sine() function distorts the veins in z-direction, because the x-coordinate determining the color is now distorted. To control the effectiveness of the noise, we add an amplitude as a scaling factor. The images above show the result for amplitude=1 and amplitude=8.

   float turbulence (vec3 P, int numFreq)
   {
      float val = 0.0;
      float freq = 1.0;
      for (int i=0; i<numFreq; i++) {
         val += abs (noise (P*freq) / freq);
         freq *= 2.07;
      }
      return val;
   }

   main ()
   {
      float amplitude = 8.0;
      const int roughness = 4;     // noisiness of veins (#octaves in turbulence)

      float t = 6.28 * vertexPosMC.x / tileSize.x ;
      t += amplitude * turbulence (vertexPosMC.xyz, roughness);
      // replicate over rows of tiles (wont be identical, because noise is depending on all coordinates of the input vector):
      t = sin(t);
      vec3 marbleColor = marble_color(t);
   }

Step 4 - Breaking it up into tiles

tiling the noise domain The screenshots so far have already included the rendering of tile boundaries, but the code did not show how they are created to keep simplicity.

Still, the veins seem to follow a periodic pattern and run all in the same major direction. The flagstones appear to be made all of the same rock from marble and ordered in the same way they were cut out.

To break these artifacts, there are several options:

  1. Break the noise domain to be different at each flagstone (rather than be continuous across them).
  2. Do not start with the same dependency on the x-coordinate. Instead, use a random (noise) 2D-vector to walk along with the sine function. the direction would not be sine(point.x), as in image 1. Instead, we'd use sine (point.x*a+point.y*(1-a))
  3. Let the veins dive into the material - this would lead to interrupted veins and fewer dark spots. We can achieve this effect by taking another noise height value and use it to blend the color of the marble generated so far with a "pure marble white", with weights depending on the height.
Choosing the second option, the fragment shaders main() function finally looks like this:
  #define Integral(x, p, np) ((floor(x)*(p)) + max(fract (x) - (np), 0.0))

  void main( void )
  {
     // Definition of tiles, in meter:
     const vec3 tileSize = vec3(1.1,1.0, 1.1);
     const vec3 tilePct = vec3(0.98,1.0, 0.98);

     // Get tile number - this adapts tileSize, transforming 0..tileSize to 0..1.
     // (factor 16 comes from vs and should be removed at both ends!):
     vec3 Tpos = vertexPosMC / tileSize;

     // move each other row of tiles:
     if (fract (Tpos.x*0.5) > 0.5)
       Tpos.z += 0.5;

     // Make position relative to tile:
     vec3 pos = fract (Tpos);

     // --- Calculate the marble color ---
     const int roughness = 4;     // noisiness of veins (#octaves in turbulence)

     vec3 tileID = ceil(Tpos); // get ID of tile, unique to a tile and common to all its pixels
     float asc    = 3*noise (2.3*(tileID));  // use this as m in t=my+x, rather than just using t=x.

     const float PI = 3.1415;
     float amplitude = 6.0;
     float t = 2.0*PI*(vertexPosMC.x + (asc*vertexPosMC.z)) / tileSize.x ;
     t += amplitude*Turbulence (vertexPosMC.xyz, roughness);
     // replicate over rows of tiles (wont be identical, because noise is depending on all coordinates of the input vector):
     t = sin(t);
     vec3 marbleColor = marble_color(t);
  
     // get filter size:
     vec3 fw = fwidth (vertexPosMC);
  
     // Determine if marble or joint: isMarble will be 0 if there is marble and 1 if we are in a joint
     // vec3 isMarble = step (pos, tilePct);
     vec3 isMarble = (Integral (pos+fw, tilePct, 1.-tilePct) - Integral (pos, tilePct, 1.-tilePct)) / fw;

     // mix the two colors together, isMarble decides which color to use:
     vec3 color = mix (vec3 (0.2, 0.2, 0.2), marbleColor, isMarble.x*isMarble.z);

     gl_FragColor = vec4 (color, alpha);
  }

Wrapping it up

The last step already produces quite convincing marble, although there is admittedly still room for improvements if you compare the last screenshot with photos of real marble. But: If you understood the steps of developing the above shader, you should also be able to enhance the code at the right places to satisfy your needs. Combine this material with per pixel lighting calculations or add shadows/reflections and it should be easy to create a glossy marble flagstone floor.

Keep rendering,
Christian


Marble flagstone room Caesar bust
Final result of floor shader. Reflections are created by rendering the scene twice with a scale factor of y=-1. Click on image to enlarge. Caesar marble bust, omitting the tiling code in the shader and using slightly different parameters for color, frequency and amplitude. Click on image to enlarge.



Acknowledgements:

  • The GLSL noise functions were written by Stefan Gustavsson and posted on OpenGL.org.
  • A wealth of ideas were taken from the orange book - The OpenGL shading language, written by R. Rost.
  • An in-depth discussion on procedural textures can be found in Texturing and Modeling - a procedural approach.


Copyright by Christian Marten, 2011
Last change: 26.12.2011