WebGL Displacement Map

Web Programming Tutorial

Overview Vertex Shader Fragment Shader JavaScript Initialize the Controller Color Grid Graphic Displacement Map Space Graphic Draw the Plane JavaScript Initialize Render Everything Summary

Overview

This tutorial covers one simple method to move vertices with a displacement map. The tutorial includes graphics, JavaScript rendering code, the vertex and fragment shaders.

The space scene and colorful grid displacement map examples, use one texture for color and a second texture for displacement.

The space and color grid graphics render directly. The shaders don't modify either image map. However the vertex shaders use the displacement graphic to change the location of vertices. The displacement map never renders directly to the screen. Vertex positions are altered based on the blue channel of texels in the displacement graphic.

Vertex shaders access the displacement graphic. Fragment shaders access either the space or color grid graphic. Upload the space or color grid graphic to a uniform Sampler2D declared in the fragment shader. Upload the displacement map to a uniform Sampler2D declared in the vertex shader.

Texture for Colorful Grid Displacement

Color Grid

Texture for Space Scene Displacement

Weird Space Image from NASA

Displacement Map

Displacement Map

Vertex Shader

The vertex shader samples just the blue channel from the displacement map based on the current texel or texture coordinate. The vertex shader modifies vertex X and Y coordinates based on the value of the blue channel and a uniform named uf_time. Uniform uf_time represents the current frame of animation. JavaScript increments and uploads uniform uf_time for each animation frame. Both frame time and values from the displacement map move vertices on the model. Vertices move over time creating a tunnel effect.

The following lines demonstrate moving vertices based on both displacement map values and animation frame time. The value assigned to vector v4_position equals X and Y coordinates divided by the product of time and the displacement map's blue channel.

v4_position.xy /= f_blue * uf_time;

Assign v4_position to built in vector gl_Position. Vector gl_Position prepares to display the vertex at it's new location.

gl_Position = v4_position; 

The following listing includes the entire vertex shader. Uniform u_sampler1 accesses the displacement map.

attribute vec4 a_position;   
attribute vec2 a_tex_coord0;

varying vec2 v_tex_coord0;
           
uniform mat4 um4_matrix;
uniform mat4 um4_pmatrix; 
uniform sampler2D u_sampler1;
uniform float uf_time; 
        
void main(void) {

 // Multiply the perspective
 // projection matrix by the 
 // model matrix and the vertex coordinates.
 vec4 v4_position = um4_pmatrix * 
  um4_matrix * 
  a_position; 
 
 // Displacement map.
 // Sample just the blue.
 float f_blue = texture2D(
  u_sampler1, 
  a_tex_coord0).b;

 // X and Y coordinates
 // divided by the
 // blue channel multiplied
 // by the animation frame.
 v4_position.xy /= f_blue * uf_time;

 // Assign orientation
 // of the current vertex.
 gl_Position = v4_position; 
 
 // Texel attributes
 // sent to fragment shader
 // through varying v_tex_coord0.
 v_tex_coord0 = a_tex_coord0;    
}

Fragment Shader

The simple fragment shader samples the either the space graphic or the colorful grid, and assigns the sample to the fragment. The entire fragment shader follows. Uniform u_sampler0 references the space graphic or the colorful grid.

precision mediump float;

// Space graphic.
uniform sampler2D u_sampler0;

// Current texel S and T:
varying vec2 v_tex_coord0;  

void main(void) {

// Assign the current
// fragment color.
gl_FragColor = texture2D(
 u_sampler0, 
 v_tex_coord0
); 
}

Displacement Shader JavaScript

The following JavaScript initializes a flat plane with two hundred fifty six squares. The more squares the more vertices to move around during rendering. The model named Plane256() declares a flat plane with 256 sections.

// Create a square plane
// composed of 256 squares.
var shapes = new Plane256();

The variable shapes includes vertices, texels, and indices to define the plane. The property array aOffset includes one offset for drawing the plane. The property array aCount includes the number of elements needed to draw the plane. The class GLEntity initializes a texture. Class GLEntity maintains the count and offset for drawing operations later.

The first parameter to GLEntity is a string representing the path to an image file. The second parameter is the texture index for the image, once it's uploaded to the GPU. The following listing demonstrates initializing two GLEntity. The first GLEntity uploads the space, or colorful grid graphics, to the fragment shader. The second GLEntity uploads the displacement graphic to the vertex shader. The e-book WebGL Textures & Vertices: Beginner's Guide explains class GLEntity in more detail.

 // Initialize the
 // entity with texture
 // from the image
 // passed as a constructor
 // parameter.
 // The image is the space graphic.
 var e =  new GLEntity(s,0);
 
 e.nOffset = Number(
  shapes.aOffset[0]
 );
 
 e.nCount = Number(
  shapes.aCount[0]
 );
 
 e.matrix[14] = -1;
 aIm.push(e);

 e =  new GLEntity(
  '../assets/displacement.jpg',
  1
 );
 
 e.nOffset = Number(
  shapes.aOffset[0]
 );
 
 e.nCount = Number(
  shapes.aCount[0]
 );
 aIm.push(e);

Initialize the Controller

The following listing demonstrates initializing a controller which uploads the plane's vertices and indices. The shapes property named aVertices includes all vertices and interleaved texels which map both graphics to the model of a flat square plane. The shapes property named aIndices includes integers for use as an element array buffer in WebGL.

The controller initializes the shaders and buffers. Seven Thunder Software doesn't rely on Three.js, Babylon.js or Unity for WebGL development. Early testing demonstrated prepared libraries don't always run on the wide variety of WebGL enabled devices. Most projects at SevenThunderSoftware.com were tested on iPhone, Android, Windows phone, and Windows PCs with WebGL enabled browsers. The e-book WebGL Textures & Vertices: Beginner's Guide explains class GLControl in more detail.

 
var controller = new GLControl
(
 shapes.aVertices,
 shapes.aIndices,  
 aIm,
 this
);  
..
};

Draw the Plane

The following listing demonstrates how simple it is to draw the plane. The variable e represents the GLEntity described earlier. Property nCount indicates how many elements to draw. Property nOffset indicates where to begin drawing, within the vertex buffer object (VBO).

// Draw the
// plane.
gl.drawElements
(
 gl.TRIANGLES,
 e.nCount,
 gl.UNSIGNED_SHORT,
 e.nOffset
);

JavaScript Initialization

Initialization prepares everything that can be processed in advance of rendering the scene. Method init() saves the location of the vertex shader's uniform named uf_time. The location is saved to property uTime within a reference to the displacement project demonstration code. Most of the WebGL projects at SevenThunderSoftware.com are named glDemo, for consistency. In this case glDemo refers to one of the displacement map projects.

Remember the vertex shader declares and uses uf_time for animation. The render() method puts it all together.

glDemo.uTime = gl.getUniformLocation(
 controller.program,
 "uf_time"
);

The following listing includes all initialization applied to the plane for viewing with WebGL. The code uses open source glMatrix class mat4 to move and scale the plane into view. Notice method init() saves the location of uf_time from the vertex shader.

GLDisplacement.prototype = {
 
/**
* Initialize WebGL
* features.
* @param controller: GLControl reference.
*/
init:function(controller) {

var gl = controller.gl;
var glDemo = controller.glDemo;
var glUtils = controller.glUtils = new GLUtils();

// Depth testing.
gl.enable(
 gl.DEPTH_TEST
);

// Black clear
// color with
// opaque alpha.
gl.clearColor(
 0.0,
 0.0,
 0.0,
 1.0
);

// Boolean representing
// backward or forward
// motion.
this.bZ = true;

// Increment per
// animation frame.
controller.N_RAD = Number(0.2);

// The plane composed of
// 256 squares.
var ePlane = controller.aEntities[0];

// Move the plane
// along the 
// Z axis.
mat4.translate(
 ePlane.matrix,
 [0,0,-64]
);

// Scale the
// plane down
// by 1/2.
mat4.scale(
ePlane.matrix,
[0.5,0.5,0.5]
);

// Upload the
// plane's matrix
// to the vertex
// shader's 
// mat4 um4_matrix
gl.uniformMatrix4fv
(
 controller.uMatrixTransform,
 false,
 newFloat32Array
(
 ePlane.matrix
)
);

// Save the vertex shader's
// uniform named uf_time.
glDemo.uTime = gl.getUniformLocation(
 controller.program,
 "uf_time"
);

// Activate the
// space texture.
gl.activeTexture(
 gl.TEXTURE0+ePlane.idx
);

// Smooth space
// texture rendering.
gl.texParameteri
(
 gl.TEXTURE_2D,
 gl.TEXTURE_MAG_FILTER,
 gl.LINEAR
);

gl.texParameteri
(
 gl.TEXTURE_2D,
 gl.TEXTURE_MIN_FILTER,
 gl.LINEAR
);

// Assign the
// animation frame
// incrementer.
controller.nRad += controller.N_RAD;

},

Render Everything

The render() method prepares everything for rendering one animation frame. The following listing demonstrates uploading the current animation frame to the vertex shader. JavaScript updates variable nRad for each animation frame. Method uniform1f() uploads the floating point value in nRad to the vertex shader's uf_time uniform.

gl.uniform1f
(
 glDemo.uTime,
 controller.nRad
);

The entire render() method follows. Method render() draws one frame at a time. Method render() uses glMatrix method mat4.rotate() to rotate the plane around the Z axis for each animation frame. Method render() uploads the modified transformation matrix and new time frame to the vertex shader.

/**
* Render one animation
* frame at a time.
*/
render: function(controller){

var gl = controller.gl;
var glDemo = controller.glDemo;
var glUtils = controller.glUtils;
var e = controller.aEntities[0];
 
 // Clear the
 // color and depth
 // buffers.
gl.clear(
 gl.COLOR_BUFFER_BIT | 
 gl.DEPTH_BUFFER_BIT
); 

// Forward or
// backward?
if (glDemo.bZ == true){
 controller.nRad += controller.N_RAD;
}
else {
 controller.nRad -= controller.N_RAD;
}
 
// Upload
// the new animation
// frame value.
gl.uniform1f
(
 glDemo.uTime,
 controller.nRad
);
 
// Rotate the
// plane around it's
// Z axis.
var matrixZ = mat4.create();
 
mat4.set(
 e.matrix,
 matrixZ
);
 
mat4.rotate(
 matrixZ, 
 controller.nRad, 
 [0, 0, 1]
);
 
// Upload the
// transformation
// matrix to the shader.
gl.uniformMatrix4fv
(
 controller.uMatrixTransform, 
 false, 
 new Float32Array
(
 matrixZ
)
); 

// Draw the
// plane.
gl.drawElements
(
 gl.TRIANGLES,
 e.nCount,
 gl.UNSIGNED_SHORT,
 e.nOffset
);

// Determine forward
// or backward motion.
if(controller.nRad > 16){
 glDemo.bZ = false;
}
else if(controller.nRad < = 1.0){
 glDemo.bZ=true;
}
},
};

Summary

This tutorial covered one simple method to move vertices with a displacement map. The tutorial included graphics, JavaScript rendering code, the vertex and fragment shaders.

Have fun and love learning!

3D Graphics Run with WebGL! Read the E-Book

WebGL Scenes: Responsive Web Design

Copyright © 2015 Seven Thunder Software. All Rights Reserved.