/**
* @file
*
* Summary.
* <p>Bump Mapping.</p>
*
* A mostly successful attempt to do bumpmapping.<br>
* Grayscale height maps are used to perturb the normals to a surface,
* making the surface look "bumpy". <br>
*
* The implementation requires tangent vectors for the surface.
*
* <p>The three object models used here have tangent vectors that can be passed
* as an attribute to the shader program. </p>
*
* Note: I haven't learned how to make the appropriate tangent vectors in general. <br>
* It took some experimentation for me to get them
* pointed in directions that seem to work.
*
* @author David J. Eck and modified by Paulo Roma
* @since 19/11/2022
* @see <a href="/WebGL/hws.edu-examples/bumpmap.html">link</a>
* @see <a href="/WebGL/hws.edu-examples/bumpmap.js">source</a>
* @see https://math.hws.edu/graphicsbook/source/webgl/bumpmap.html
* @see https://math.hws.edu/eck/cs424/downloads/graphicsbook-linked.pdf#page=312
*/
"use strict";
const mat4 = glMatrix.mat4;
const mat3 = glMatrix.mat3;
/**
* The webgl context.
* @type {WebGLRenderingContext}
*/
let gl;
/**
* Canvas on which gl draws.
* @type {CanvasRenderingContext2D}
*/
let canvas;
/**
* Vertex coordinates location.
* @type {GLuint}
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getAttribLocation
*/
let a_coords_loc;
/**
* Vertex normal location.
* @type {GLuint}
*/
let a_normal_loc;
/**
* Vertex tangents location.
* @type {GLuint}
*/
let a_tangent_loc;
/**
* Vertex texture coordinates location.
* @type {GLuint}
*/
let a_texCoords_loc;
/**
* Holds uniform location for model view matrix.
* @type {WebGLUniformLocation}
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getUniformLocation
*/
let u_modelview;
/**
* Holds uniform location for projection matrix.
* @type {WebGLUniformLocation}
*/
let u_projection;
/**
* Holds uniform location for normal matrix.
* @type {WebGLUniformLocation}
*/
let u_normalMatrix;
/**
* Holds uniform location for front material.
* @type {WebGLUniformLocation}
*/
let u_material;
/**
* Holds uniform location for light properties.
* @type {WebGLUniformLocation}
*/
let u_lights;
/**
* An image texture.
* @type {WebGLUniformLocation}
*/
let u_texture;
/**
* Tells whether to use texture for diffuseColor.
* @type {WebGLUniformLocation}
*/
let u_useTexture;
/**
* A bumpmap texture (grayscale).
* @type {WebGLUniformLocation}
*/
let u_bumpmap;
/**
* An array giving bumpmap texture size.
* @type {WebGLUniformLocation}
*/
let u_bumpmapSize;
/**
* A value telling how strong the bump effect is (can be negative).
* @type {WebGLUniformLocation}
*/
let u_bumpmapStrength;
/**
* Projection matrix.
* @type {mat4}
*/
const projection = mat4.create();
/**
* Modelview matrix; value comes from rotator.
* @type {mat3}
*/
let modelview;
/**
* Matrix, derived from modelview matrix, for transforming normal vectors.
* @type {mat3}
*/
const normalMatrix = mat3.create();
/**
* A TrackballRotator to implement rotation by mouse.
* @type {TrackballRotator}
*/
let rotator;
/**
* Array of objects, containing models created using {@link createModel}.
* Will contain a cube, a cylinder and a torus.
* @type {Array<model>}
*/
const objects = [];
/**
* The image texture.
* @type {WebGLTexture}
*/
let texture;
/**
* The bumpmap texture.
* @type {WebGLTexture}
*/
let bumpmap;
let textureLoading = false;
let bumpmapLoading = false;
/**
* Color array.
* @type {Array<Array<Number>>}
*/
const colors = [
[1, 1, 1],
[1, 0.5, 0.5],
[1, 1, 0.5],
[0.5, 1, 0.5],
[0.5, 0.5, 1],
];
/**
* Bump map images.
* @type {Array<String>}
*/
const bumpmapURLs = [
"textures/dimples-height-map.png",
"textures/marble-height-map.png",
"textures/brick-height-map.jpg",
"textures/metal-height-map.png",
"textures/random-height-map.png",
];
/**
* Render the scene.
*/
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (textureLoading || bumpmapLoading) {
return;
}
let strength = Number(document.getElementById("strength").value);
gl.uniform1f(u_bumpmapStrength, strength);
modelview = rotator.getViewMatrix();
let objectNum = Number(document.getElementById("object").value);
objects[objectNum].render();
}
/**
* <p>Create an object representing an IFS model.</p>
* The modelData holds the data for an IFS using the structure from {@link basic-object-models-with-tangents-IFS.js}.<br>
* This function creates VBOs to hold the coordinates, normal vectors, tangent vectors, <br>
* texture coordinates and indices from the IFS. It also loads the data into those buffers.
*
* <p>The function creates a new object whose properties are the identifiers of the VBOs.<br>
* The new object also has a function, {@link render}, that can be called to
* render the object, using all the data from the buffers. <br>
* That object is returned as the value of the function.</p>
* @param {Object.<{vertexPositions: Float32Array, vertexNormals: Float32Array, vertexTextureCoords: Float32Array, vertexTangents:Float32Array, indices: Uint16Array}>} modelData
* @property {object} model
* @property {WebGLBuffer} model.coordsBuffer - coordinate buffer.
* @property {WebGLBuffer} model.normalBuffer - normal buffer.
* @property {WebGLBuffer} model.tangentBuffer - tangent buffer.
* @property {WebGLBuffer} model.texCoordsBuffer - texture buffer.
* @property {WebGLBuffer} model.indexBuffer - index buffer.
* @property {Number} model.count - number of indices.
* @property {render} model.render - render function.
* @returns {model} created model.
*/
function createModel(modelData) {
let model = {};
model.coordsBuffer = gl.createBuffer();
model.normalBuffer = gl.createBuffer();
model.tangentBuffer = gl.createBuffer();
model.texCoordsBuffer = gl.createBuffer();
model.indexBuffer = gl.createBuffer();
model.count = modelData.indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, model.coordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, modelData.vertexPositions, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, model.normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, modelData.vertexNormals, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, model.tangentBuffer);
gl.bufferData(gl.ARRAY_BUFFER, modelData.vertexTangents, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, model.texCoordsBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
modelData.vertexTextureCoords,
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, modelData.indices, gl.STATIC_DRAW);
/**
* <p>This function will render the object.</p>
* Since the buffer from which we are taking the coordinates and normals
* change each time an object is drawn, <br>
* we have to use gl.vertexAttribPointer to specify the location of the data.
*
* <p>To accomplish that, we must first bind the buffer that contains the data. <br>
* Similarly, we have to bind this object's index buffer before calling gl.drawElements.</p>
* @callback render
*/
model.render = function () {
gl.bindBuffer(gl.ARRAY_BUFFER, this.coordsBuffer);
gl.vertexAttribPointer(a_coords_loc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.normalBuffer);
gl.vertexAttribPointer(a_normal_loc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.tangentBuffer);
gl.vertexAttribPointer(a_tangent_loc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordsBuffer);
gl.vertexAttribPointer(a_texCoords_loc, 2, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(u_modelview, false, modelview);
mat3.normalFromMat4(normalMatrix, modelview);
gl.uniformMatrix3fv(u_normalMatrix, false, normalMatrix);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.drawElements(gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0);
};
return model;
}
/**
* Loads the bumpmap texture and passes it to the fragment shader.
*/
function loadBumpmap() {
document.getElementById("message").innerHTML = "LOADING BUMPMAP TEXTURE";
let bumpmapNum = Number(document.getElementById("bumpmap").value);
bumpmapLoading = true;
draw();
let img = new Image();
img.onload = function () {
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, bumpmap);
try {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
img
);
} catch (e) {
document.getElementById("message").innerHTML =
"SORRY, COULD NOT ACCESS BUMPMAP TEXTURE IMAGE.";
return;
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
bumpmapLoading = false;
document.getElementById("message").innerHTML =
"Drag on the object to rotate it.";
gl.uniform2f(u_bumpmapSize, img.width, img.height);
draw();
};
img.onerror = function () {
document.getElementById("message").innerHTML =
"SORRY, COULDN'T LOAD BUMPMAP TEXTURE IMAGE";
};
img.src = bumpmapURLs[bumpmapNum];
document.getElementById("bumpimage").src = bumpmapURLs[bumpmapNum];
}
/**
* Sets diffuse color in the fragment shader and calls {@link draw}.
*/
function setDiffuse() {
let colorNum = Number(document.getElementById("color").value);
gl.uniform1i(u_useTexture, 0);
gl.uniform3fv(u_material.diffuseColor, colors[colorNum]);
console.log(colorNum);
draw();
}
/**
* <p>Initialize the WebGL context. </p>
* Called from {@link init}.
*/
function initGL() {
// load and compile the shader pair
var vertexShaderSource =
document.getElementById("vertex-shader").textContent;
var fragmentShaderSource =
document.getElementById("fragment-shader").textContent;
let prog = createProgram(gl, vertexShaderSource, fragmentShaderSource);
gl.useProgram(prog);
gl.enable(gl.DEPTH_TEST);
/* Get attribute and uniform locations and create the buffers */
a_coords_loc = gl.getAttribLocation(prog, "a_coords");
a_normal_loc = gl.getAttribLocation(prog, "a_normal");
a_tangent_loc = gl.getAttribLocation(prog, "a_tangent");
a_texCoords_loc = gl.getAttribLocation(prog, "a_texCoords");
gl.enableVertexAttribArray(a_normal_loc);
gl.enableVertexAttribArray(a_tangent_loc);
gl.enableVertexAttribArray(a_coords_loc);
gl.enableVertexAttribArray(a_texCoords_loc);
u_modelview = gl.getUniformLocation(prog, "modelview");
u_projection = gl.getUniformLocation(prog, "projection");
u_normalMatrix = gl.getUniformLocation(prog, "normalMatrix");
u_texture = gl.getUniformLocation(prog, "texture");
u_useTexture = gl.getUniformLocation(prog, "useTexture");
u_bumpmap = gl.getUniformLocation(prog, "bumpmap");
u_bumpmapSize = gl.getUniformLocation(prog, "bumpmapSize");
u_bumpmapStrength = gl.getUniformLocation(prog, "bumpmapStrength");
gl.uniform1i(u_useTexture, 0);
gl.uniform1i(u_texture, 0);
gl.uniform1i(u_bumpmap, 1);
texture = gl.createTexture();
bumpmap = gl.createTexture();
u_material = {
diffuseColor: gl.getUniformLocation(prog, "material.diffuseColor"),
specularColor: gl.getUniformLocation(prog, "material.specularColor"),
specularExponent: gl.getUniformLocation(
prog,
"material.specularExponent"
),
};
u_lights = new Array(3);
for (let i = 0; i < 3; i++) {
u_lights[i] = {
enabled: gl.getUniformLocation(prog, "lights[" + i + "].enabled"),
position: gl.getUniformLocation(prog, "lights[" + i + "].position"),
color: gl.getUniformLocation(prog, "lights[" + i + "].color"),
};
}
/* Set up values for material and light uniforms; these values don't change in this program. */
gl.uniform3f(u_material.diffuseColor, 1, 1, 1);
gl.uniform3f(u_material.specularColor, 0.2, 0.2, 0.2);
gl.uniform1f(u_material.specularExponent, 32);
for (let i = 0; i < 3; i++) {
gl.uniform1i(u_lights[i].enabled, 0);
}
gl.uniform1i(u_lights[0].enabled, 1); // in the end, I decided to use only the viewpoint light
gl.uniform4f(u_lights[0].position, 0, 0, 1, 0);
gl.uniform3f(u_lights[0].color, 0.6, 0.6, 0.6);
gl.uniform4f(u_lights[1].position, -1, -1, 1, 0);
gl.uniform3f(u_lights[1].color, 0.3, 0.3, 0.3);
gl.uniform4f(u_lights[2].position, 0, 3, -1, 0);
gl.uniform3f(u_lights[2].color, 0.3, 0.3, 0.3);
mat4.perspective(projection, Math.PI / 10, 1, 1, 10);
gl.uniformMatrix4fv(u_projection, false, projection);
objects[0] = createModel(uvCone(0.6, 1, 24, false));
objects[1] = createModel(cube(0.85));
objects[2] = createModel(uvCylinder());
objects[3] = createModel(uvTorus(0.65, 0.2, 64, 24));
objects[4] = createModel(uvSphere(0.65, 64, 24));
mat4.perspective(projection, Math.PI / 10, 1, 1, 10);
gl.uniformMatrix4fv(u_projection, false, projection);
gl.clearColor(250 / 255, 235 / 255, 215 / 255, 1);
}
/**
* <p>Creates a program for use in the WebGL context gl, and returns the
* identifier for that program. </p>
*
* If an error occurs while compiling or linking the program,
* an exception of type Error is thrown. The error
* string contains the compilation or linking error.
*
* <p>If no error occurs, the program identifier is the return value of the function.
* The second and third parameters are strings that contain the
* source code for the vertex shader and for the fragment shader.</p>
* @param {WebGLProgram} gl WebGL context.
* @param {WebGLShader} vShader vertex shader.
* @param {WebGLShader} fShader fragment shade.
*/
function createProgram(gl, vShader, fShader) {
let vsh = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vsh, vShader);
gl.compileShader(vsh);
if (!gl.getShaderParameter(vsh, gl.COMPILE_STATUS)) {
throw new Error("Error in vertex shader: " + gl.getShaderInfoLog(vsh));
}
let fsh = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fsh, fShader);
gl.compileShader(fsh);
if (!gl.getShaderParameter(fsh, gl.COMPILE_STATUS)) {
throw new Error(
"Error in fragment shader: " + gl.getShaderInfoLog(fsh)
);
}
let prog = gl.createProgram();
gl.attachShader(prog, vsh);
gl.attachShader(prog, fsh);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error(
"Link error in program: " + gl.getProgramInfoLog(prog)
);
}
return prog;
}
/**
* Initialization function that will be called when the page has loaded.
*/
function init() {
try {
canvas = document.getElementById("webglcanvas");
gl = canvas.getContext("webgl");
if (!gl) {
throw "Browser does not support WebGL";
}
} catch (e) {
document.getElementById("message").innerHTML =
"<p>Sorry, could not get a WebGL graphics context.</p>";
return;
}
try {
initGL(); // initialize the WebGL graphics context
} catch (e) {
document.getElementById("message").innerHTML =
"<p>Sorry, could not initialize the WebGL graphics context: " +
e.message +
"</p>";
return;
}
document.getElementById("reset").onclick = function () {
rotator.setView(5, [2, 2, 3]);
draw();
};
let aspect = canvas.width / canvas.height;
/**
* Screen events.
*/
function handleWindowResize() {
let h = window.innerHeight;
let w = window.innerWidth;
if (h > w) {
h = w / aspect; // aspect < 1
} else {
w = h * aspect; // aspect > 1
}
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
// mat4.perspective(projection, Math.PI / 10, aspect, 1, 10);
}
window.addEventListener("resize", handleWindowResize, false);
handleWindowResize();
document.getElementById("bumpmap").value = "0";
document.getElementById("bumpmap").onchange = loadBumpmap;
document.getElementById("object").value = "3";
document.getElementById("object").onchange = draw;
document.getElementById("color").value = "1";
document.getElementById("color").onchange = setDiffuse;
document.getElementById("strength").value = "3";
document.getElementById("strength").onchange = draw;
rotator = new TrackballRotator(canvas, draw, 5, [2, 2, 3]);
loadBumpmap();
setDiffuse();
}
window.addEventListener("load", (event) => init());