You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

726 lines
24 KiB
JavaScript

/**
* A basic Web GL class. This provides a very basic setup for GLSL shader code.
* Currently it doesn't support anything except for clip-space 3d, but this was
* done so that we could start writing fragments right out of the gate. My
* Intention is to update it with particle and polygonal 3d support later on.
*
* @class WTCGL
* @author Liam Egan <liam@wethecollective.com>
* @version 0.0.8
* @created Jan 16, 2019
*/
class WTCGL {
/**
* The WTCGL Class constructor. If construction of the webGL context fails
* for any reason this will return null.
*
* @TODO make the dimension properties properly optional
* @TODO provide the ability to allow for programmable buffers
*
* @constructor
* @param {HTMLElement} el The canvas element to use as the root
* @param {string} vertexShaderSource The vertex shader source
* @param {string} fragmentShaderSource The fragment shader source
* @param {number} [width] The width of the webGL context. This will default to the canvas dimensions
* @param {number} [height] The height of the webGL context. This will default to the canvas dimensions
* @param {number} [pxratio=1] The pixel aspect ratio of the canvas
* @param {boolean} [styleElement] A boolean indicating whether to apply a style property to the canvas (resizing the canvas by the inverse of the pixel ratio)
* @param {boolean} [webgl2] A boolean indicating whether to try to create a webgl2 context instead of a regulart context
*/
constructor(el, vertexShaderSource, fragmentShaderSource, width, height, pxratio, styleElement, webgl2) {
this.run = this.run.bind(this);
this._onRun = ()=>{};
// Destructure if an object is aprovided instead a series of parameters
if(el instanceof Object && el.el) {
({el, vertexShaderSource, fragmentShaderSource, width, height, pxratio, webgl2, styleElement} = el);
}
// If the HTML element isn't a canvas, return null
if(!el instanceof HTMLElement || el.nodeName.toLowerCase() !== 'canvas') {
console.log('Provided element should be a canvas element');
return null;
}
this._el = el;
// The context should be either webgl2, webgl or experimental-webgl
if(webgl2 === true) {
this.isWebgl2 = true;
this._ctx = this._el.getContext("webgl2", this.webgl_params) || this._el.getContext("webgl", this.webgl_params) || this._el.getContext("experimental-webgl", this.webgl_params);
} else {
this.isWebgl2 = false;
this._ctx = this._el.getContext("webgl", this.webgl_params) || this._el.getContext("experimental-webgl", this.webgl_params);
}
// Set up the extensions
this._ctx.getExtension('OES_standard_derivatives');
this._ctx.getExtension('EXT_shader_texture_lod');
this._ctx.getExtension('OES_texture_float');
this._ctx.getExtension('WEBGL_color_buffer_float');
this._ctx.getExtension('OES_texture_float_linear');
this._ctx.getExtension('EXT_color_buffer_float');
// We can't make the context so return an error
if (!this._ctx) {
console.log('Browser doesn\'t support WebGL ');
return null;
}
// Create the shaders
this._vertexShader = WTCGL.createShaderOfType(this._ctx, this._ctx.VERTEX_SHADER, vertexShaderSource);
this._fragmentShader = WTCGL.createShaderOfType(this._ctx, this._ctx.FRAGMENT_SHADER, fragmentShaderSource);
// Create the program and link the shaders
this._program = this._ctx.createProgram();
this._ctx.attachShader(this._program, this._vertexShader);
this._ctx.attachShader(this._program, this._fragmentShader);
this._ctx.linkProgram(this._program);
// If we can't set up the params, this means the shaders have failed for some reason
if (!this._ctx.getProgramParameter(this._program, this._ctx.LINK_STATUS)) {
console.log('Unable to initialize the shader program: ' + this._ctx.getProgramInfoLog(this._program));
return null;
}
// Initialise the vertex buffers
this.initBuffers([
-1.0, 1.0, -1.,
1.0, 1.0, -1.,
-1.0, -1.0, -1.,
1.0, -1.0, -1.,
]);
// Initialise the frame buffers
this.frameBuffers = [];
// The program information object. This is essentially a state machine for the webGL instance
this._programInfo = {
attribs: {
vertexPosition: this._ctx.getAttribLocation(this._program, 'a_position'),
},
uniforms: {
projectionMatrix: this._ctx.getUniformLocation(this._program, 'u_projectionMatrix'),
modelViewMatrix: this._ctx.getUniformLocation(this._program, 'u_modelViewMatrix'),
resolution: this._ctx.getUniformLocation(this._program, 'u_resolution'),
time: this._ctx.getUniformLocation(this._program, 'u_time'),
},
};
// Tell WebGL to use our program when drawing
this._ctx.useProgram(this._program);
this.pxratio = pxratio;
this.styleElement = styleElement !== true;
this.resize(width, height);
}
/**
* Public methods
*/
addFrameBuffer(w, h, tiling = 0, buffertype = 0) {
// create to render to
const gl = this._ctx;
const targetTextureWidth = w * this.pxratio;
const targetTextureHeight = h * this.pxratio;
const targetTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, targetTexture);
{
// define size and format of level 0
const level = 0;
let internalFormat = gl.RGBA;
const border = 0;
let format = gl.RGBA;
let t;
if(buffertype & WTCGL.TEXTYPE_FLOAT) {
const e = gl.getExtension('OES_texture_float');
window.extension = e;
t = e.FLOAT;
// internalFormat = gl.RGBA32F;
} else if(buffertype & WTCGL.TEXTYPE_HALF_FLOAT_OES) {
// t = gl.renderer.isWebgl2 ? e.HALF_FLOAT : e.HALF_FLOAT_OES;
// gl.renderer.extensions['OES_texture_half_float'] ? gl.renderer.extensions['OES_texture_half_float'].HALF_FLOAT_OES :
// gl.UNSIGNED_BYTE;
const e = gl.getExtension('OES_texture_half_float');
t = this.isWebgl2 ? gl.HALF_FLOAT : e.HALF_FLOAT_OES;
// format = gl.RGBA;
if(this.isWebgl2) {
internalFormat = gl.RGBA16F;
}
// internalFormat = gl.RGB32F;
// format = gl.RGB32F;
// window.gl = gl
// t = e.HALF_FLOAT_OES;
} else {
t = gl.UNSIGNED_BYTE;
}
const type = t;
const data = null;
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
targetTextureWidth, targetTextureHeight, border,
format, type, data);
// gl.generateMipmap(gl.TEXTURE_2D);
// set the filtering so we don't need mips
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Set the parameters based on the passed type
if(tiling === WTCGL.IMAGETYPE_TILE) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
} else if(tiling === WTCGL.IMAGETYPE_MIRROR) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
} else if(tiling === WTCGL.IMAGETYPE_REGULAR) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
}
// Create and bind the framebuffer
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
// attach the texture as the first color attachment
const attachmentPoint = gl.COLOR_ATTACHMENT0;
const level = 0;
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);
return {
w: w * this.pxratio,
h: h * this.pxratio,
fb: fb,
frameTexture: targetTexture
};
}
/**
* Resizes the canvas to a specified width and height, respecting the pixel ratio
*
* @param {number} w The width of the canvas
* @param {number} h The height of the canvas
* @return {Void}
*/
resize(w, h) {
this.width = w;
this.height = h;
this._el.width = w * this.pxratio;
this._el.height = h * this.pxratio;
this._size = [w * this.pxratio, h * this.pxratio];
if(this.styleElement) {
this._el.style.width = w + 'px';
this._el.style.height = h + 'px';
}
this._ctx.viewportWidth = w * this.pxratio;
this._ctx.viewportHeight = h * this.pxratio;
this._ctx.uniform2fv( this._programInfo.uniforms.resolution, this._size);
this.initBuffers(this._positions);
}
/**
* Initialise a provided vertex buffer
*
* @param {array} positions The vertex positions to initialise
* @return {Void}
*/
initBuffers(positions) {
this._positions = positions;
this._positionBuffer = this._ctx.createBuffer();
this._ctx.bindBuffer(this._ctx.ARRAY_BUFFER, this._positionBuffer);
this._ctx.bufferData(this._ctx.ARRAY_BUFFER,
new Float32Array(positions),
this._ctx.STATIC_DRAW);
}
/**
* Add a uniform to the program. At this time the following types are supported:
* - Float - WTCGL.TYPE_FLOAT
* - Vector 2 - WTCGL.TYPE_V2
* - Vector 3 - WTCGL.TYPE_V3
* - Vector 4 - WTCGL.TYPE_V4
*
* @param {string} name The name of the uniform. N.B. your name will be prepended with a `u_` in your shaders. So providing a name of `foo` here will result in a uniform named `u_foo`
* @param {WTCGL.UNIFORM_TYPE} type The unfiform type
* @param {number|array} value The unfiform value. The type depends on the uniform type being created
* @return {WebGLUniformLocation} The uniform location for later reference
*/
addUniform(name, type, value) {
let uniform = this._programInfo.uniforms[name];
uniform = this._ctx.getUniformLocation(this._program, `u_${name}`);
switch(type) {
case WTCGL.TYPE_INT :
if(!isNaN(value)) this._ctx.uniform1i( uniform, value);
break;
case WTCGL.TYPE_FLOAT :
if(!isNaN(value)) this._ctx.uniform1f( uniform, value);
break;
case WTCGL.TYPE_V2 :
if(value instanceof Array && value.length === 2.) this._ctx.uniform2fv( uniform, value);
break;
case WTCGL.TYPE_V3 :
if(value instanceof Array && value.length === 3.) this._ctx.uniform3fv( uniform, value);
break;
case WTCGL.TYPE_V4 :
if(value instanceof Array && value.length === 4.) this._ctx.uniform4fv( uniform, value);
break;
case WTCGL.TYPE_BOOL :
if(!isNaN(value)) this._ctx.uniform1i( uniform, value);
break;
}
this._programInfo.uniforms[name] = uniform;
return uniform;
}
/**
* Adds a texture to the program and links it to a named uniform. Providing the type changes the tiling properties of the texture. Possible values for type:
* - WTCGL.IMAGETYPE_REGULAR - No tiling, clamp to edges and doesn't need to be power of 2.
* - WTCGL.IMAGETYPE_TILE - full x and y tiling, needs to be power of 2.
* - WTCGL.IMAGETYPE_MIRROR - mirror tiling, needs to be power of 2.
*
* @public
* @param {string} name The name of the uniform. N.B. your name will be prepended with a `u_` in your shaders. So providing a name of `foo` here will result in a uniform named `u_foo`
* @param {WTCGL.TYPE_IMAGETYPE} type The type of texture to create. This is basically the tiling behaviour of the texture as described above
* @param {Image} image The image object to add to the texture
* @return {WebGLTexture} The texture object
*/
addTexture(name, type, image, liveUpdate = false) {
var texture = this._ctx.createTexture();
this._ctx.pixelStorei(this._ctx.UNPACK_FLIP_Y_WEBGL, true);
this._ctx.bindTexture(this._ctx.TEXTURE_2D, texture);
// this._ctx.generateMipmap(this._ctx.TEXTURE_2D);
// Set the parameters based on the passed type
if(type === WTCGL.IMAGETYPE_MIRROR) {
this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_WRAP_S, this._ctx.MIRRORED_REPEAT);
this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_WRAP_T, this._ctx.MIRRORED_REPEAT);
} else if(type === WTCGL.IMAGETYPE_REGULAR) {
this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_WRAP_S, this._ctx.CLAMP_TO_EDGE);
this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_WRAP_T, this._ctx.CLAMP_TO_EDGE);
}
this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_MIN_FILTER, this._ctx.LINEAR);
// this._ctx.texParameteri(this._ctx.TEXTURE_2D, this._ctx.TEXTURE_MAG_FILTER, this._ctx.LINEAR);
// Upload the image into the texture.
this._ctx.texImage2D(this._ctx.TEXTURE_2D, 0, this._ctx.RGBA, this._ctx.RGBA, this._ctx.UNSIGNED_BYTE, image);
// add the texture to the array of textures.
this.pushTexture(name, texture, image, this._ctx.TEXTURE_2D, liveUpdate);
return texture;
}
pushTexture(name, texture, image, target, liveUpdate = false) {
let textures = this.textures;
textures.push({ name: name, tex: texture, liveUpdate: liveUpdate, image: image, target: target });
// Finally set the this.textures (this is just to get around the funnyness of default getters)
this.textures = textures;
}
/**
* Updates a texture location for a given WebGLTexture with an image
*
* @param {WebGLTexture} texture The texture location to update
* @param {Image} image The image object to add to the texture
* @return {Void}
*/
updateTexture(texture, image, name) {
let uniform = this._ctx.getUniformLocation(this._program, `u_${name}`);
// Set the texture unit to the uniform
this._ctx.uniform1i(uniform, 0);
this._ctx.activeTexture(this._ctx.TEXTURE0);
this._ctx.bindTexture(this._ctx.TEXTURE_2D, texture);
// Upload the image into the texture.
this._ctx.texImage2D(this._ctx.TEXTURE_2D, 0, this._ctx.RGBA, this._ctx.RGBA, this._ctx.UNSIGNED_BYTE, image);
}
/**
* Initialise texture locations in the program
*
* @return {Void}
*/
initTextures() {
for(let i = 0; i < this.textures.length; i++) {
let name = this.textures[i].name;
let uniform = this._programInfo.uniforms[name];
uniform = this._ctx.getUniformLocation(this._program, `u_${name}`);
// Set the texture unit to the uniform
this._ctx.uniform1i(uniform, i);
// find the active texture based on the index
this._ctx.activeTexture(this._ctx[`TEXTURE${i}`]);
// Finally, bind the texture
this._ctx.bindTexture(this.textures[i].target, this.textures[i].tex);
}
}
/**
* The run loop. This function is run as a part of a RaF and updates the internal
* time uniform (`u_time`).
*
* @param {number} delta The delta time provided by the RaF loop
* @return {Void}
*/
run(delta) {
this.running && requestAnimationFrame(this.run);
const runFunction = () => {
this.time = this.startTime + delta * .0002;
this.onRun(delta);
this.render();
}
if(this.frameRate) {
let now = Date.now();
let elapsed = now - this._then;
if (elapsed > this.frameRate) {
this._then = now - (elapsed % this.frameRate);
runFunction();
}
} else {
runFunction();
}
}
/**
* Render the program
*
* @return {Void}
*/
render(buffer = {}) {
this._ctx.bindFramebuffer(this._ctx.FRAMEBUFFER, buffer.fb || null);
// Update the time uniform
this._ctx.uniform1f( this._programInfo.uniforms.time, this.time);
this.textures.forEach((textureInfo) => {
if(textureInfo.liveUpdate === true) {
this.updateTexture(textureInfo.tex, textureInfo.image, textureInfo.name);
}
});
this._ctx.viewport(0, 0, buffer.w || this._ctx.viewportWidth, buffer.h || this._ctx.viewportHeight);
if(this.clearing) {
this._ctx.clearColor(1.0, 0.0, 0.0, 0.0);
// this._ctx.clearDepth(1.0);
// this._ctx.enable(this._ctx.DEPTH_TEST);
// this._ctx.depthFunc(this._ctx.LEQUAL);
this._ctx.blendFunc(this._ctx.SRC_ALPHA, this._ctx.ONE_MINUS_SRC_ALPHA);
this._ctx.clear( this._ctx.COLOR_BUFFER_BIT );
}
this._ctx.bindBuffer(this._ctx.ARRAY_BUFFER, this._positionBuffer);
this._ctx.vertexAttribPointer(
this._programInfo.attribs.vertexPosition,
3,
this._ctx.FLOAT,
false,
0,
0);
this._ctx.enableVertexAttribArray(this._programInfo.attribs.vertexPosition);
// Set the shader uniforms
this.includePerspectiveMatrix && this._ctx.uniformMatrix4fv( this._programInfo.uniforms.projectionMatrix, false, this.perspectiveMatrix);
this.includeModelViewMatrix && this._ctx.uniformMatrix4fv( this._programInfo.uniforms.modelViewMatrix, false, this.modelViewMatrix);
this._ctx.drawArrays(this._ctx.TRIANGLE_STRIP, 0, 4);
}
/**
* Getters and setters
*/
/**
* The default webGL parameters to be used for the program.
* This is read only and should only be overridden as a part of a subclass.
*
* @readonly
* @type {object}
* @default { alpha: true }
*/
get webgl_params() {
return { alpha: true };
}
/**
* (getter/setter) Whether the element should include styling as a part of
* its rendition.
*
* @type {boolean}
* @default true
*/
set styleElement(value) {
this._styleElement = value === true;
if(this._styleElement === false && this._el) {
this._el.style.width = '';
this._el.style.height = '';
}
}
get styleElement() {
return this._styleElement !== false;
}
/**
* (getter/setter) startTime. This is a value to begin the `u_time`
* unform at. This is here in case you want `u_time` to begin at a
* specific value other than 0.
*
* @type {number}
* @default 0
*/
set startTime(value) {
if(!isNaN(value)) {
this._startTime = value;
}
}
get startTime() {
return this._startTime || 0;
}
/**
* (getter/setter) time. This is the time that the program currently
* sits at. By default this value is set as a part of the run loop
* however this is a public property so that we can specify time
* for rendition outside of the run loop.
*
* @type {number}
* @default 0
*/
set time(value) {
if(!isNaN(value)) {
this._time = value;
}
}
get time() {
return this._time || 0;
}
/**
* (getter/setter) includePerspectiveMatrix. This determines whether the
* perspecive matrix is included in the program. This doesn't really make
* a difference right now, but this is here to provide future interoperability.
*
* @type {boolean}
* @default false
*/
set includePerspectiveMatrix(value) {
this._includePerspectiveMatrix = value === true;
}
get includePerspectiveMatrix() {
return this._includePerspectiveMatrix === true;
}
/**
* (getter/setter) includeModelViewMatrix. This determines whether the
* model view matrix is included in the program. This doesn't really make
* a difference right now, but this is here to provide future interoperability.
*
* @type {boolean}
* @default false
*/
set includeModelViewMatrix(value) {
this._includeModelViewMatrix = value === true;
}
get includeModelViewMatrix() {
return this._includeModelViewMatrix === true;
}
/**
* (getter/setter) textures. The array of textures to initialise into the program.
*
* @private
* @type {array}
* @default []
*/
set textures(value) {
if(value instanceof Array) {
this._textures = value;
}
}
get textures() {
return this._textures || [];
}
/**
* (getter/setter) clearing. Specifies whether the program should clear the screen
* before drawing anew.
*
* @type {boolean}
* @default false
*/
set clearing(value) {
this._clearing = value === true;
}
get clearing() {
return this._clearing === true;
}
/**
* (getter/setter) running. Specifies whether the programming is running. Setting
* this to true will create a RaF loop which will call the run function.
*
* @type {boolean}
* @default false
*/
set running(value) {
if(!this.running && value === true) {
this._then = Date.now();
requestAnimationFrame(this.run);
}
this._running = value === true;
}
get running() {
return this._running === true;
}
set frameRate(value) {
if(!isNaN(value)) this._frameRate = 1000 / value;
}
get frameRate() {
return this._frameRate || null;
}
/**
* (getter/setter) pxratio. The 1-dimensional pixel ratio of the application.
* This should be used either for making a program look good on high density
* screens or for raming down pixel density for performance.
*
* @type {number}
* @default 1
*/
set pxratio(value) {
if(value > 0) this._pxratio = value;
}
get pxratio() {
return this._pxratio || 1;
}
/**
* (getter/setter) perspectiveMatrix. Calculate a perspective matrix, a
* special matrix that is used to simulate the distortion of perspective in
* a camera. Our field of view is 45 degrees, with a width/height ratio
* that matches the display size of the canvas and we only want to see
* objects between 0.1 units and 100 units away from the camera.
*
* @readonly
* @type {mat4}
*/
get perspectiveMatrix() {
const fieldOfView = 45 * Math.PI / 180; // in radians
const aspect = this._size.w / this._size.h;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
// note: glmatrix.js always has the first argument
// as the destination to receive the result.
mat4.perspective(projectionMatrix,
fieldOfView,
aspect,
zNear,
zFar);
return projectionMatrix;
}
/**
* (getter/setter) perspectiveMatrix. Calculate a model view matrix.
*
* @readonly
* @type {mat4}
*/
get modelViewMatrix() {
// Set the drawing position to the "identity" point, which is
// the center of the scene.
const modelViewMatrix = mat4.create();
// Now move the drawing position a bit to where we want to
// start drawing the square.
mat4.translate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[-0.0, 0.0, -1.]); // amount to translate
return modelViewMatrix;
}
set onRun(runMethod) {
if(typeof runMethod == 'function') {
this._onRun = runMethod.bind(this);
}
}
get onRun() {
return this._onRun;
}
get context() {
return this._ctx || null;
}
/**
* Static Methods
*/
/**
* Create a shader of a given type given a context, type and source.
*
* @static
* @param {WebGLContext} ctx The context under which to create the shader
* @param {WebGLShaderType} type The shader type, vertex or fragment
* @param {string} source The shader source.
* @return {WebGLShader} The created shader
*/
static createShaderOfType(ctx, type, source) {
const shader = ctx.createShader(type);
ctx.shaderSource(shader, source);
ctx.compileShader(shader);
if (!ctx.getShaderParameter(shader, ctx.COMPILE_STATUS)) {
console.log('An error occurred compiling the shaders: ' + ctx.getShaderInfoLog(shader));
ctx.deleteShader(shader);
return null;
}
return shader;
}
}
WTCGL.TYPE_INT = 0;
WTCGL.TYPE_FLOAT = 1;
WTCGL.TYPE_V2 = 2;
WTCGL.TYPE_V3 = 3;
WTCGL.TYPE_V4 = 4;
WTCGL.TYPE_BOOL = 5;
WTCGL.IMAGETYPE_REGULAR = 0;
WTCGL.IMAGETYPE_TILE = 1;
WTCGL.IMAGETYPE_MIRROR = 2;
WTCGL.TEXTYPE_FLOAT = 0;
WTCGL.TEXTYPE_UNSIGNED_BYTE = 1;
WTCGL.TEXTYPE_HALF_FLOAT_OES = 2;