Procedural Solid Texturing

2010, Dr. Lawlor, CS 481/681, CS, UAF

"Procedural" means via a procedure, like a GLSL program (not just a fixed lookup table).
"Solid" means going through 3D space, like via a vec3 location (not just a 2D image!).
"Texturing" means a complicated and real-world surface appearance.

For example, you can draw a checkerboard grid with:
vec4 grid(vec2 loc) {
vec2 gridcorner=floor(loc);
vec2 gridfrac=loc-gridcorner;
if (gridfrac.x<0.5 ^ gridfrac.y<0.5)
return vec4(1,0,0,1); // red
else


		return vec4(0,0,0,1); // black
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)


Or you can draw a grid of dots with:
vec4 grid(vec2 loc) {
vec2 gridcorner=floor(loc);
vec2 gridcenter=gridcorner+vec2(0.5);
float radius=length(loc-gridcenter);
if (radius<0.3)
return vec4(1,0,0,1); // red
else
return vec4(0,0,0,1); // black
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)



You can change the dots' colors, by looking up color values from a texture:
vec4 grid(vec2 loc) {
vec2 gridcorner=floor(loc);
vec2 gridcenter=gridcorner+vec2(0.5);
float radius=length(loc-gridcenter);
if (radius<0.3)
return texture2D(tex1,texcoords); // read texture 3
else
return vec4(0,0,0,1); // black
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)



If you want consistent colors across the dots, you need to look colors up based on the dot's center, not the current location:
vec4 grid(vec2 loc) {
vec2 gridcorner=floor(loc);
vec2 gridcenter=gridcorner+vec2(0.5);
float radius=length(loc-gridcenter);
if (radius<0.3)
return texture2D(tex1,gridcenter*0.1); // read texture 3
else
return vec4(0,0,0,1); // black
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

You can even move the dots, again by fetching a dot-center based shift vector.  Note that I can only shift the circle's center so far, until it begins to hit the boundaries between grid cells.  Here, with a radius=0.3 circle, I can shift the center to lie between between 0.3 and 0.7, and the entire circle stays inside my grid cell.
vec4 grid(vec2 loc) {
vec2 gridcorner=floor(loc);
vec2 gridshift=0.4*texture2D(tex1,gridcorner*0.1234);
vec2 gridcenter=gridcorner+vec2(0.3)+gridshift;
float radius=length(loc-gridcenter);
if (radius<0.3)
return texture2D(tex1,gridcenter*0.1); // read texture 3
else
return vec4(0,0,0,1); // black
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

To move dots farther than one grid cell, I need to search over all the adjacent grid corners:
vec4 grid(vec2 loc) {
vec4 ret=vec4(0.0);
vec2 gridcorner=floor(loc);
for (float dy=-1.0;dy<=1.0;dy++)
for (float dx=-1.0;dx<=1.0;dx++)
{
vec2 thiscorner=gridcorner+vec2(dx,dy);
vec2 gridshift=2.4*texture2D(tex4,thiscorner*3.456);
vec2 center=thiscorner+vec2(-0.7)+gridshift;
float radius=length(loc-center);
if (radius<0.3)
ret+=texture2D(tex1,center*0.1);
}
return ret;
}

(Try this in NetRun now!)

With this approach, just the distance to the nearest grid cell actually looks fairly interesting, giving a series of dots reminiscent of a voronoi diagram.  This is a "cellular" texture; read more about these at gamedev (for pretty pictures).
vec4 grid(vec2 loc) {
float dist=10.0; // distance to closest grid cell
vec2 gridcorner=floor(loc);
for (float dy=-1.0;dy<=1.0;dy++)
for (float dx=-1.0;dx<=1.0;dx++)
{
vec2 thiscorner=gridcorner+vec2(dx,dy);
vec2 gridshift=texture2D(tex4,thiscorner*3.456);
vec2 center=thiscorner+gridshift;
float radius=length(loc-center);
dist=min(radius,dist);
}
return vec4(dist);
}

(Try this in NetRun now!)

Here's the distance to the *second* nearest grid point; I had to expand my search radius slightly to find points.  This effect is described in Worley's 1996 paper.
vec4 grid(vec2 loc) {
float dist=10.0; // distance to closest grid cell
float second=10.0; // distance to the next-nearest grid cell
vec2 gridcorner=floor(loc);
for (float dy=-2.0;dy<=2.0;dy++)
for (float dx=-2.0;dx<=2.0;dx++)
{
vec2 thiscorner=gridcorner+vec2(dx,dy);
vec2 gridshift=texture2D(tex4,thiscorner*3.456);
vec2 center=thiscorner+gridshift;
float radius=length(loc-center);
if (radius<dist) { // new best
second=dist;
dist=radius;
} else if (radius<second) {
second=radius; // new second-best
}
}
return vec4(second*0.7);
}

(Try this in NetRun now!)

Here's the distance to the second-nearest point, minus the distance to the nearest point, which gives a cool stained-glass effect.
vec4 grid(vec2 loc) {
float dist=10.0; // distance to closest grid cell
float second=10.0; // distance to the next-nearest grid cell
vec2 gridcorner=floor(loc);
for (float dy=-2.0;dy<=2.0;dy++)
for (float dx=-2.0;dx<=2.0;dx++)
{
vec2 thiscorner=gridcorner+vec2(dx,dy);
vec2 gridshift=texture2D(tex4,thiscorner*3.456);
vec2 center=thiscorner+gridshift;
float radius=length(loc-center);
if (radius<dist) { // new best
second=dist;
dist=radius;
} else if (radius<second) {
second=radius; // new second-best
}
}
return 2.0*vec4(second-dist);
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

Another recurring feature is "multi-octave" effects.  For example, here's a single repeating texture; a high note:
vec4 grid(vec2 loc) {
return texture2D(tex4,loc);
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

Here's the same texture stretched out to a lower frequency:
vec4 grid(vec2 loc) {
return texture2D(tex4,loc*0.5);
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

Here are the two, added together:
vec4 grid(vec2 loc) {
vec4 high=texture2D(tex4,loc);
vec4 low=texture2D(tex4,loc*0.5);
return 0.5*(high+low);
}
void main(void) {
gl_FragColor=grid(texcoords*10.0);
}

(Try this in NetRun now!)

Here are many octaves added together (this looks better with a noisy repeating image):
vec4 grid(vec2 loc) {
vec4 ret=vec4(0.0);
for (float freq=8.0;freq>=0.25;freq*=0.5)
ret+=texture2D(tex4,loc*freq);
return (1.0/6.0)*ret;
}
void main(void) {
gl_FragColor=grid(texcoords);
}

(Try this in NetRun now!)



Finally, here's a very famous procedural texture: the mandelbrot set.
void main(void) {
float cx=texcoords.x, cy=texcoords.y; // location onscreen
float x=cx, y=cy; // point to iterate
float r=0.0; // radius of point (squared)

for (int i=0;i<100;i++) //<- repeat!
{
float newx=x*x - y*y;
y=2*x*y; // <- complex multiplication

x=newx+cx; y=y+cy; // bias by onscreen location

r=x*x+y*y; // check radius
if (r>4.0) break;
}
gl_FragColor=vec4(x,r*0.1,y,0);
}

(Try this in NetRun now!)


The interesting part about these effects is that they are entirely procedural--they can be computed on the fly at each pixel, without any precomputation or rasterized image to be found!