Procedural Investigations in webGL – Section II

Section II:
Uniforms and UI


With the ability to create the likeness of a brick wall we can now start adding some controls that will allow the testing of various parameter values in real time. There would be a multitude of ways to handle this the most simple being using HTML DOM elements. If you are feeling froggy you could attempt to use the BABYLON.GUI system, which is GPU accelerated. The first steps will be to extend our SM object to be able to add controls quickly.

A Uniform Argument

Right away we go to where the SM object is constructed, go to the argument object and then add a new variable for the uniform. It is here we will define the uniforms name, type, value, and any constraints that will be used later with the UI.

...
sm = new SM(
{
uniforms:{
	brickCounts : {
		type : 'vec2',
		value : new BABYLON.Vector2(6,12),
		min : new BABYLON.Vector2(1,1),
		step : new BABYLON.Vector2(1,1),
		hasControl : true
	}
},
fx :...

Then we need to give the SM object instructions on what to do with this new argument.

SM = function(args, scene){	
...
    this.shaders.fx = args.fx || this.shaders.fx;	
    this.uniforms = args.uniforms || {};			
    this.uID = 'shader'+Date.now();
...
    return this;
}

Now that the argument is stored on the object, we need to modify some of the object methods to accommodate for the new uniforms. The function most effected by this change is the buildShader method.
We need to make sure that when we bind our shader that we include our new uniforms and then set their default values.

...
buildShader : function(){
...
    var _uniforms = ["world", "worldView", "worldViewProjection", "view", "projection"];
    _uniforms =  _uniforms.concat(this.getUniformsArray());
	
    var shader = new BABYLON.ShaderMaterial("shader", scene, {
        vertex: uID,
        fragment: uID,
        },{
        attributes: ["position", "normal", "uv"],
        uniforms: _uniforms
});
...

Then to make these changes work we need to define a method for grabbing the array of uniform names that are assigned. We could simply use Object.keys(this.uniforms); everytime we wanted to get that array of names, but that is a little ugly and redundant.

...
SM.prototype = {
getUniformsArray : function(){
var keys = Object.keys(this.uniforms);
return keys;
},
buildOutput :...

Before we go to much farther, it would be prudent to modify our fragment shader being passed in the arguments to accommodate for this new uniform otherwise when we try to set the default value the shader will not compile. We also have no need for the #define XBRICKS and #define YBRICKS, with the new uniform effectively replacing these variables.

...
fx :
`precision highp float;
//Varyings
...
//Methods
...
/*----- UNIFORMS ------*/
uniform vec2 brickCounts;

#define MWIDTH 0.1

void main(void) {	
    vec3 brickColor = vec3(1.,0.,0.);
    vec3 mortColor = vec3(0.55);	
	
    vec2 brickSize = vec2(
        1.0/brickCounts.x,
        1.0/brickCounts.y
    );
	
    vec2 pos = vUV/brickSize;
	
    vec2 mortSize = 1.0-vec2(MWIDTH*(brickCounts.x/brickCounts.y), MWIDTH);
	
    pos += mortSize*0.5;	
	
    if(fract(pos.y * 0.5) > 0.5){
        pos.x += 0.5;   
    }	

    pos = fract(pos);	
	
    vec2 brickOrMort = step(pos, mortSize);
	
    vec3 color =  mix(mortColor, brickColor, brickOrMort.x * brickOrMort.y);

    gl_FragColor = vec4(color, 1.0);
}...

If you are following along, and were to refresh the page now you would most likely see a solid grey page. This is because the shader can bind no problem as there should not be any errors but with the brick counts set to 0 the math fails. We solve this by doing a little more work on the SM object to have it set the defaults values of the uniforms after the shader is bound.

...
SM.prototype = {
getUniformsArray : ...
},
setUniformDefaults : function(){
    var shader = this.shader;
    var keys = this.getUniformsArray();
    for(var i=0; i<keys.length; i++){
        var u = this.uniforms[keys[i]];
        var type = u.type;
        var v = u.value;		
        shader[this.type2Method(type)](keys[i], v);	 // <== shader.setType( uniform, value );	
    }
},
type2Method : function(type){
    var m;
    switch(type){
        case 'float': m ='setFloat'; break;
        case 'vec2': m ='setVector2'; break;
        case 'vec3': m ='setVector3'; break;
    }
    return m;
},
buildOutput :...
},
buildShader : function(){
...
    this.shader = shader;	
    this.setUniformDefaults();
...
}

This may look a little intimidating, but its really not. First we get the key values of the uniforms (the names). Then we iterate through these keys now and grab the default value and the type. Once we have the type we need to get back the associated method that BJS has for setting the uniforms on the shader. In this situation the line “shader[this.type2Method(type)](keys[i], v);” essentially becomes shader.setVector2(‘brickCounts’, BABYLON.Vector2(#,#)); If everything is correct when we refresh the page now we should see whatever number of bricks we set as the default values on the constructors arguments. Feel free to change these numbers up and refresh the page to verify everything is working. You can look HERE for reference or to download this step.

With everything lined up and working, its now time to get the UI elements constructed. Eventually you might want to develop your own user interface components, but the process I am about to show you should cover most cases. For simplicity of code understanding I am going to write out some sections of code that repeat with little variation. Normally want to have function handle these repeat sections, but it will be easier to understand initially to do it long handed. The creation of the UI can be easily be expanded upon in the future, but to get started we create another method on our SM object, then call it after the creation of the output on the initialization. Now would also be a good time to define a quick support method to return the current “this.shader”.


SM = function(args, scene){	
    ...
    this.buildOutput();
	
    this.buildGUI();
	
    return this;
}

SM.prototype = {
...
getShader : function(){
return this.shader;
},
buildGUI : function(){
    this.ui = {
        mainBlock : document.createElement('div'),
        inputs : [],
    };
	
    this.ui.mainBlock.classList.add('ui-block');
	
    var keys = this.getUniformsArray();	
	
},
buildOutput:...

The purpose of this method will be to iterate through the SM object’s uniform object keys, create all the appropriate DOM elements, append them and then set a function up to respond to change events. So we set up a new container object for the ui elements, then grab the uniform keys with our getUniformArray method. Once we have our keys to iterate through we proceed to parse the uniforms object data.

...
    var keys = this.getUniformsArray();	
    for(var i=0; i<keys.length; i++){
        var u = this.uniforms[keys[i]];		
        if(!u.hasControl){continue;}		
		
        var _block = document.createElement('div');
        _block.classList.add('ui-item');
		
        var _title = document.createElement('span');
        _title.innerHTML = keys[i]+":";
        _block.appendChild(_title);
        
        var _inBlock = document.createElement('span');
        _inBlock.classList.add('ui-in-block');
	var _inputs = [];
	var _in;
		
	if(u.type == 'float'){
	    _in = document.createElement('input');	
	    _in.setAttribute('type', 'number');
	    _in.setAttribute('id', keys[i]);
	    _in.classList.add('ui-in-'+u.type, 'ui-in');
	    _in.value = u.value;
	    if(u.min){
	            _in.setAttribute('min', u.min.x);
	    }
	    if(u.max){
	        _in.setAttribute('max', u.max.x);
	    }
	    if(u.step){
	        _in.setAttribute('step', u.step.x);
	    }	
	    _inputs.push(_in);
	}
		
	if(u.type == 'vec2' || u.type == 'vec3'){
	    _in = document.createElement('input');	
	    _in.setAttribute('type', 'number');
	    _in.setAttribute('id', keys[i]+":x");
	    _in.classList.add('ui-in-'+u.type, 'ui-in');
	    _in.value = u.value.x;			
	    if(u.min){
	        _in.setAttribute('min', u.min.x);
	    }
	    if(u.max){
	        _in.setAttribute('max', u.max.x);
	    }
	    if(u.step){
	        _in.setAttribute('step', u.step.x);
	    }
	    _inputs.push(_in);
	    _in = document.createElement('input');	
	    _in.setAttribute('type', 'number');
	    _in.setAttribute('id', keys[i]+":y");
	    _in.classList.add('ui-in-'+u.type, 'ui-in');
	    _in.value = u.value.y;			
	    if(u.min){
	        _in.setAttribute('min', u.min.y);
	    }
	    if(u.max){
	        _in.setAttribute('max', u.max.y);
	    }
	    if(u.step){
	        _in.setAttribute('step', u.step.y);
	    }
	    _inputs.push(_in);
	}
	if(u.type == 'vec3'){
	    _in = document.createElement('input');	
	    _in.setAttribute('type', 'number');
	    _in.setAttribute('id', keys[i]+":z");
	    _in.classList.add('ui-in-'+u.type, 'ui-in');
	    _in.value = u.value.z;			
	    if(u.min){
		_in.setAttribute('min', u.min.z);
	    }
	    if(u.max){
		_in.setAttribute('max', u.max.z);
	    }
	    if(u.step){
		_in.setAttribute('step', u.step.z);
	    }
	    _inputs.push(_in);
	}
		
	for(var j=0; j<_inputs.length; j++){
            _inBlock.appendChild(_inputs[j]);
	}
		
	_block.appendChild(_inBlock);
		
	var _input = {
	    block : _block,
	    inputs : _inputs
	};
	    this.ui.inputs.push(_input);
	    this.ui.mainBlock.appendChild(_input.block);
    }	
    document.body.appendChild(this.ui.mainBlock);
...
}

With this added into our method, we can now (hopefully) support the creation of DOM inputs for floats, vector2, vector3 parameters. I have not tested any of it yet and am kinda writing all of this as we go so bare with me if their are any bugs and you are reading a version that is not finalized/debugged. But as far as I can tell right now this should work. If we were to refresh the page you would not see any changes, unless you look at the source. In order to see the changes we will need to provide some CSS. You can simply copy this next section and modify it how ever you want.

<style>
    html, body {
        overflow: hidden;
        width   : 100%;
        height  : 100%;
        margin  : 0;
        padding : 0;
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        box-sizing: border-box;
    }
		
    *, *:before, *:after {
        -webkit-box-sizing: inherit;
        -moz-box-sizing: inherit;
        box-sizing: inherit;
    }

    #renderCanvas {
        width   : 100%;
        height  : 100%;
        touch-action: none;
    }
		
    .ui-block{
        display:block;
        position:absolute;
        left:0;
        top:0;
        z-index:10001;
        background:rgba(150,150,150,0.5);
        width:240px;
        font-size:16px;
        font-family: Arial, Helvetica, sans-serif;
    }
		
    .ui-item{
        display:block;
        position:relative;
        padding:0.2em 0.5em;
    }
		
    .ui-in-block{
        display:inline-block;
        width:60%;
        white-space:nowrap;
    }
		
    .ui-in{
        display:inline-block;
        width:100%;
    }
		
    .ui-in-vec2{
        width:50%;
    }
		
   .ui-in-vec3{
        width:32.5%;
   }
		
</style>

Upon a refresh now, we should see our UI elements for the brickCounts Uniform on the top left of our page. Then we go back to our buildGUI method in order to script the responses to change events on the ui block.

...
document.body.appendChild(this.ui.mainBlock);
    
    var self = this;

    function updateShaderValue(id, value){		
        if(id.length>1){
            self.uniforms[id[0]].value[id[1]] = parseFloat(value);			
            if(id[1]=='vec2'){
		(self.getShader()).setVector2(id[0], self.uniforms[id[0]].value);	
	    }else if(id[1]=='vec3'){
		(self.getShader()).setVector3(id[0], self.uniforms[id[0]].value);	
	    }		
	}else{
	    self.uniforms[id[0]].value = parseFloat(value);
	    (self.getShader()).setFloat(id[0], self.uniforms[id[0]].value);				
	}			
    }
	
//BINDINGS//	
    this.ui.mainBlock.addEventListener('change', (e)=>{
	var target = e.target;
	var id = target.getAttribute('id').split(':');		
	var value = target.value;
	updateShaderValue(id, value);		
    }, false);
	
}

VoilĂ  it is done… partially. Upon refreshing the page then changing one of the values in our inputs we should instantly see the values in our shader being updated(effecting the output). Now to go back and add support for a few more parameters like the colors and the mortar width. If we set up everything correctly we can now just edit our arguments and change the fragment shader slightly.

...
uniforms:{
	brickCounts : {
		type : 'vec2',
		value : new BABYLON.Vector2(6,12),
		min : new BABYLON.Vector2(1,1),
		step : new BABYLON.Vector2(1,1),
		hasControl : true
	},
	mortarSize : {
		type: 'float',
		value : 0.1,
		min: 0.0001,
		max: 0.9999,
		step: 0.0001,
		hasControl: true
	},
	brickColor : {
		type: 'vec3',
		value : new BABYLON.Vector3(0.8, 0.1, 0.1),
		min: new BABYLON.Vector3(0, 0, 0),
		max: new BABYLON.Vector3(1, 1, 1),
		step: new BABYLON.Vector3(0.001, 0.001, 0.001),
		hasControl: true
	},
	mortColor : {
		type: 'vec3',
		value : new BABYLON.Vector3(0.35, 0.35, 0.35),
		min: new BABYLON.Vector3(0, 0, 0),
		max: new BABYLON.Vector3(1, 1, 1),
		step: new BABYLON.Vector3(0.001, 0.001, 0.001),
		hasControl: true
	},
},
fx :
`precision highp float;
//Varyings
varying vec2 vUV;
varying vec2 tUV;

//Methods
float pulse(float a, float b, float v){
return step(a,v) - step(b,v);
}
float pulsate(float a, float b, float v, float x){
return pulse(a,b,mod(v,x)/x);
}
float gamma(float g, float v){
	return pow(v, 1./g);
}
float bias(float b, float v){
	return pow(v, log(b)/log(0.5));
}
float gain(float g, float v){
	if(v < 0.5){
	return bias(1.0-g, 2.0*v)/2.0;
	}else{
	return 1.0 - bias(1.0-g, 2.0 - 2.0*v)/2.0;
	}
}

/*----- UNIFORMS ------*/
uniform vec2 brickCounts;
uniform float mortarSize;
uniform vec3 brickColor;
uniform vec3 mortColor;

void main(void) {	
	
	vec2 brickSize = vec2(
		1.0/brickCounts.x,
		1.0/brickCounts.y
	);
	
	vec2 pos = vUV/brickSize;
	
	vec2 mortSize = 1.0-vec2(mortarSize*(brickCounts.x/brickCounts.y), mortarSize);
	
	pos += mortSize*0.5;	
	
	if(fract(pos.y * 0.5) > 0.5){
     	pos.x += 0.5;   
    }
	
	pos = fract(pos);	
	
	vec2 brickOrMort = step(pos, mortSize);
	
	vec3 color =  mix(mortColor, brickColor, brickOrMort.x * brickOrMort.y);

    gl_FragColor = vec4(color, 1.0);
}`...

Now that is procedural, take a little bit of time to mess around with this and experiment with some different values now that you can see the changes instantly! If you are having trouble getting to this point you can review and/or download the source here.

Exporting/Saving

All these parameters and options for bricks is cool and all… but now we should start making the changes necessary to make the texture exportable which will be the easiest way to take this texture we made from mock up to production. Eventually a good end goal would be to include these procedural process in your project and have them compile on runtime, which could save tons of space when saving/serving the project to a client if used correctly. But that is much much later, for now lets focus on making the ability to set the textures size and then saving it from the browser. Because the HTML canvas object is processed by the browser for all intensive purposes as an image, we can simply right click on the canvas and save it! After a couple quick changes to our SM object and a small change to the DOM structure, we can add one additional argument to set the size of the canvas to a specific unit.


//DOM CHANGES
...
<body>
<div id='output-block'>
<canvas id="renderCanvas"></canvas>
</div>
...

//SM OBJECT CHANGES
SM = function(args, scene){	
	...
	this.buildGUI();
	
	if(args.size){this.setSize(args.size);}
	
	return this;
}

SM.prototype = {
setSize : function(size){
	var canvas = this.scene._engine._gl.canvas;
	var pNode = canvas.parentNode;
	pNode.style.width = size.x+'px';
	pNode.style.height = size.y+'px';
	this.scene._engine.resize();
	this.buildOutput();		
},
...

//ADD ARGUMENT
sm = new SM(
{
size : new BABYLON.Vector2(512, 512),
uniforms:{...

The one thing we need to make sure we do when we change the size of the canvas manually, is to fire the engines resize function to get the gl context into the same dimensions. We then rebuild the output just to be safe. Now we have a useful brick wall generator that we can export textures from for later use. Here is the final source for this section and a live example of the generator we just created.

Continue to Section III