Procedural Investigations in webGL – Section III

Section III
Advance Spaces, Time and Polar


With our new SM object put together we now have the ability to start putting together a collection of generators and other GLSL functions to create more dynamic content. If we go back to our reference book [1] starting on page 46 it starts reviewing some interesting methods we will recreate now.

Star of My Eye


For practice a great project is to create a star shape. At first glance one might think that it would be tough to generate something like this, but once we shift our sampling space to be in polar coordinates through cos/sin (sinusoidal) calculations. Again my version will be a variation of a segment of script meant for Renderman. I will show the RSL (Renderman Shader Language) version from [1] next to my GLSL version then review the differences. I would recommend going through line by line and try to recreate this on your own.


surface star(
    uniform float Ka = 1;
    uniform float Kd = 1;
    uniform color starcolor = color (1.0000,0.5161,0.0000);
    uniform float npoints = 5;
    uniform float sctr = 0.5;
    uniform float tctr = 0.5;
){
    point Nf = normalize(faceforward(N, I));
    color Ct;
    float ss, tt, angle, r, a, in_out;
    uniform float rmin = 0.07, rmax = 0.2;
    uniform float starangle = 2*PI/npoints;
    uniform point pO = rmax*(cos(0),sin(0), 0);
    uniform point pi = rmin*(cos(starangle/2),sin(starangle/2),0);
    uniform point d0 = pi - p0; point d1;
    ss = s - sctr; tt=t- tctr;
    angle = atan(ss, tt) + PI;
    r = sqrt(ss*ss + tt*tt);
    a = mod(angle, starangle)/starangle;
    if (a >= 0.5) a = 1 - a;
    dl = r*(cos(a), sin(a),0) - p0;
    in_out = step(0, zcomp(d0^d1) );
    Ct = mix(Cs, starcolor, in_out);
    /* diffuse (“matte”) shading model */
    Oi = Os;
    Ci = Os * Ct * (Ka * ambient() + Kd * diffuse(Nf));
}

precision highp float;
//Varyings
varying vec2 vUV;
varying vec2 tUV;
//Methods
/*----- UNIFORMS ------*/
#define PI 3.14159265359
uniform vec3 starColor;
uniform float nPoints;
uniform float rmin;
uniform float rmax;
uniform float aaValue;
uniform vec3 bgColor;

void main(void) {
	vec2 offsetFix = vec2(0.5);	
	float ss, tt, angle, r, a;
	vec3 color = bgColor;		
	float starAngle = 2.*PI/nPoints;	
	vec3 p0 = rmax*vec3(cos(0.),sin(0.), 0.);
	vec3 p1 = rmin*vec3(cos(starAngle/2.),sin(starAngle/2.), 0.);	
	vec3 d0 = p1 - p0;
	vec3 d1;	
	
	ss = vUV.x - offsetFix.x; tt = (1.0 - vUV.y) - offsetFix.y;
	angle = atan(ss, tt) + PI;	
	r = sqrt(ss*ss + tt*tt);		
	a = mod(angle, starAngle)/starAngle;
	
	if (a >= 0.5){a = 1.0 - a;}
	
	d1 = r*vec3(cos(a), sin(a), 0.) - p0;
	
	float in_out = smoothstep(0., aaValue, cross(d0 , d1).z);	

	color = mix(color, starColor, in_out);	

    gl_FragColor = vec4(color, 1.0);	
}

Some of the values in the RSL version are irrelevant to us, like Ka, Kd, Nf, Oi, Ci and any other variables accosted with a light model. Things that are important to us are the number of points the star will have, its size limits, and our colors. Lets go through the GLSL version line by line and understand what is going on.

First we have our precision mode, which we want as accurate as floats as possible so we keep it as highp. The varying section is pretty standard, we could remove the tUV as its not used but we will keep it as you may want to sample in the -1 to 1 range instead of 0 to 1 in some instances. No additional methods need to be defined. The uniforms section includes a definition for PI, because we are going to be working in polar space and be using calculations dependent on circular/spherical values. GLSL does not define this value inherently and so it is up to us to make sure we have a value we can reference; the cool part about this is we can experiment with funky values and see how that effects our calculations (PI = 4, for example).

The main function of the program starts with up setting a value for the offset fix of the star, which we will use later to move the sample into a scope that will ‘center’ the star. Then we define a few floats that will be used later, they could be defined at time of execution of the line this just makes things more readable. I cant even lie, I do not understand this math in the slightest… I understand some of it, but for the most part I just translated it from RSL to GLSL. Even the explanation in [1] is kinda crap as well. If any math buffs are reading this and want to do a break down of wtf is going on with these numbers and can send me an email, I will love you long time.

At the very least here is a snippet of the summary from [1]:

To test whether (r,a) is inside the star, the shader finds the vectors d0 from the
tip of the star point to the rmin vertex and d1 from the tip of the star point to the
sample point. Now we use a handy trick from vector algebra. The cross product of
two vectors is perpendicular to the plane containing the vectors, but there are two
directions in which it could point. If the plane of the two vectors is the (x, y) plane,
the cross product will point along the positive z-axis or along the negative z-axis.
The direction in which it points is determined by whether the first vector is to the left
or to the right of the second vector. So we can use the direction of the cross product
to decide which side of the star edge d0 the sample point is on.

Yeah… what that says… Its pretty much a distance function, anyways one improvement I included was the ability to anti-alias the edges. This is very simple, we just change out the step calculation for a smoothstep one with a decently low value to represent the tolerance.

There are other ways to go about this but for now this will do. Mess around with this a little bit see what you can figure out. For a live example you can go here.

Head in the Clouds & Introducing Time


Another common procedural processes would be the creation 2d/3d clouds. There are way to many solutions for this then I could count, but a very simple implementation would be to layer multiple sinusoidal functions at different frequencies. I think now would be a good time to implement some time shifting to our shader as well. We will use this time shift to animate the clouds. Once again lets take a look at the RSL version provided in [1] and compare it to my GLSL solution.


#define NTERMS 5
surface cloudplane(
    color cloudcolor = color (1,1,1);
)
{
    color Ct;
    point Psh;
    float i, amplitude, f;
    float x, fx, xfreq, xphase;
    float y, fy, yfreq, yphase;
    uniform float offset = 0.5;
    uniform float xoffset = 13;
    uniform float yoffset = 96;
    Psh = transform(“shader”, P);
    x = xcomp(Psh) + xoffset;
    y = ycomp(Psh) + yoffset;
    xphase = 0.9; /* arbitrary */
    yphase = 0.7; /* arbitrary */
    xfreq = 2 * PI * 0.023;
    yfreq = 2 * PI * 0.021;
    amplitude = 0.3;
    f = 0;
    for (i = 0; i < NTERMS; i += 1) {
        fx = amplitude * (offset + cos(xfreq * (x + xphase)));
        fy = amplitude * (offset + cos(yfreq * (y + yphase)));
        f += fx * fy;
        xphase = PI/2 * 0.9 * cos (yfreq * y);
        yphase = PI/2 * 1.1 * cos (xfreq * x);
        xfreq *= 1.9+i* 0.1; /* approximately 2 */
        yfreq *= 2.2-i* 0.08; /* approximately 2 */
        amplitude *= 0.707;
    }
    f = clamp(f, 0, 1);
    Ct = mix(Cs, cloudcolor, f);
    Oi = Os;
    Ci = Os * Ct;
    }
}

precision highp float;

uniform float time;
//Varyings
varying vec2 vUV;
varying vec2 tUV;

//Methods

/*----- UNIFORMS ------*/
#define PI 3.14159265359
uniform vec3 cloudColor;
uniform vec3 bgColor;

uniform float zoom;
uniform float octaves;
uniform float amplitude;

uniform vec2 offsets;

void main(void) {
    float f = 0.0;
    vec2 phase = vec2(0.9*time, 0.7);
    vec2 freq = vec2(2.0*PI*0.023, 2.0*PI*0.021);    
    
    float offset = 0.5;
    vec2 pos = vec2(vUV.x+offsets.x, vUV.y+offsets.y);	
    
    float scale = 1.0/zoom;
	
    pos.x = pos.x*scale + offset + time;
    pos.y = pos.y*scale + offset - sin(time*0.32);
	
    float amp = amplitude;
    
    for(float i = 0.0; i < octaves; i++){
        float fx = amp * (offset + cos(freq.x * (pos.x + phase.x)));
        float fy = amp * (offset + cos(freq.y * (pos.y + phase.y)));
        f += fx * fy;
        phase.x = PI/2.0 * 0.9 * cos(freq.y * pos.y);
        phase.y = PI/2.0 * 1.1 * cos(freq.x * pos.x);
        amp *= 0.602;
        freq.x *= 1.9 + i * .01;
        freq.y *= 2.2 - i * 0.08;
    }
	
    f = clamp(f, 0., 1.);	
    vec3 color = mix(bgColor, cloudColor, f);
 	
    gl_FragColor = vec4(color, 1.0);	
}

This is a very specific form of procedural generation that relies on a method called Spectral Synthesis. This process is described by the theory of Fourier analysis which states that functions can be represented as a sum several sinusoidal terms. We sample these functions at different frequencies and phases to generate a result. The main struggle with this method is preventing tiling or noticeable patterns which ruin the effect. The implementation of this is very limited as it relies on quite a few “magic numbers” and is not as customization as more modern solutions using noise algorithms.

The major difference with the GLSL version that I have introduced here is the animation aspect. We achieve this first by making some modifications to our SM object to accommodate.

SM = function(args, scene){	
...	
    this.uID = 'shader'+Date.now();
	
    this.hasTime = args.hasTime || false;
    this.timeFactor = args.timeFactor || 1;
    this._time = 0;
	
    this.shader = null;	
    ...
};

SM.prototype = {
setTime : function(delta){
    this._time += delta*this.timeFactor;
    if(this.shader){
        this.shader.setFloat('time', this._time);
    }
},

...

engine.runRenderLoop(function(){
     scene.render();
     if(sm.hasTime){
        var d = scene.getAnimationRatio();
	sm.setTime(d);
     }
});
...

Then we add the arguments to when we call our new SM object.

...
sm = new SM(
{
size : new BABYLON.Vector2(512, 512),
hasTime : true,
timeFactor : 0.1,
uniforms:{
...

We could simply add a value to the time variable, but in order to sync it between different clients we use BJS method of scene.getAnimationRatio(). This should keep the shaders time coordinates at the same value if they started at the same time but have different thread speeds. Mess around with this generator and try different stuff out just to get more comfortable with what is going on.

For a live example you can go here.

Continue to Section IV

References

1. TEXTURING & MODELING – A Procedural Approach (third edition)