Building a better marbleIntroductionMarble 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 veinsBasically, 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 glowAn 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 turbulenceObviously, 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.
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.
Step 4 - Breaking it up into tilesThe 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:
#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 upThe 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, Acknowledgements:
Copyright by Christian Marten, 2011 Last change: 26.12.2011 |