ogl
Version:
WebGL Library
1,477 lines (1,264 loc) • 280 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.ogl = {}));
}(this, (function (exports) { 'use strict';
/**
* Calculates the length of a vec3
*
* @param {vec3} a vector to calculate length of
* @returns {Number} length of a
*/
function length(a) {
let x = a[0];
let y = a[1];
let z = a[2];
return Math.sqrt(x * x + y * y + z * z);
}
/**
* Copy the values from one vec3 to another
*
* @param {vec3} out the receiving vector
* @param {vec3} a the source vector
* @returns {vec3} out
*/
function copy(out, a) {
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
return out;
}
/**
* Set the components of a vec3 to the given values
*
* @param {vec3} out the receiving vector
* @param {Number} x X component
* @param {Number} y Y component
* @param {Number} z Z component
* @returns {vec3} out
*/
function set(out, x, y, z) {
out[0] = x;
out[1] = y;
out[2] = z;
return out;
}
/**
* Adds two vec3's
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {vec3} out
*/
function add(out, a, b) {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
out[2] = a[2] + b[2];
return out;
}
/**
* Subtracts vector b from vector a
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {vec3} out
*/
function subtract(out, a, b) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
out[2] = a[2] - b[2];
return out;
}
/**
* Multiplies two vec3's
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {vec3} out
*/
function multiply(out, a, b) {
out[0] = a[0] * b[0];
out[1] = a[1] * b[1];
out[2] = a[2] * b[2];
return out;
}
/**
* Divides two vec3's
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {vec3} out
*/
function divide(out, a, b) {
out[0] = a[0] / b[0];
out[1] = a[1] / b[1];
out[2] = a[2] / b[2];
return out;
}
/**
* Scales a vec3 by a scalar number
*
* @param {vec3} out the receiving vector
* @param {vec3} a the vector to scale
* @param {Number} b amount to scale the vector by
* @returns {vec3} out
*/
function scale(out, a, b) {
out[0] = a[0] * b;
out[1] = a[1] * b;
out[2] = a[2] * b;
return out;
}
/**
* Calculates the euclidian distance between two vec3's
*
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {Number} distance between a and b
*/
function distance(a, b) {
let x = b[0] - a[0];
let y = b[1] - a[1];
let z = b[2] - a[2];
return Math.sqrt(x * x + y * y + z * z);
}
/**
* Calculates the squared euclidian distance between two vec3's
*
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {Number} squared distance between a and b
*/
function squaredDistance(a, b) {
let x = b[0] - a[0];
let y = b[1] - a[1];
let z = b[2] - a[2];
return x * x + y * y + z * z;
}
/**
* Calculates the squared length of a vec3
*
* @param {vec3} a vector to calculate squared length of
* @returns {Number} squared length of a
*/
function squaredLength(a) {
let x = a[0];
let y = a[1];
let z = a[2];
return x * x + y * y + z * z;
}
/**
* Negates the components of a vec3
*
* @param {vec3} out the receiving vector
* @param {vec3} a vector to negate
* @returns {vec3} out
*/
function negate(out, a) {
out[0] = -a[0];
out[1] = -a[1];
out[2] = -a[2];
return out;
}
/**
* Returns the inverse of the components of a vec3
*
* @param {vec3} out the receiving vector
* @param {vec3} a vector to invert
* @returns {vec3} out
*/
function inverse(out, a) {
out[0] = 1.0 / a[0];
out[1] = 1.0 / a[1];
out[2] = 1.0 / a[2];
return out;
}
/**
* Normalize a vec3
*
* @param {vec3} out the receiving vector
* @param {vec3} a vector to normalize
* @returns {vec3} out
*/
function normalize(out, a) {
let x = a[0];
let y = a[1];
let z = a[2];
let len = x * x + y * y + z * z;
if (len > 0) {
//TODO: evaluate use of glm_invsqrt here?
len = 1 / Math.sqrt(len);
}
out[0] = a[0] * len;
out[1] = a[1] * len;
out[2] = a[2] * len;
return out;
}
/**
* Calculates the dot product of two vec3's
*
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {Number} dot product of a and b
*/
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
/**
* Computes the cross product of two vec3's
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @returns {vec3} out
*/
function cross(out, a, b) {
let ax = a[0],
ay = a[1],
az = a[2];
let bx = b[0],
by = b[1],
bz = b[2];
out[0] = ay * bz - az * by;
out[1] = az * bx - ax * bz;
out[2] = ax * by - ay * bx;
return out;
}
/**
* Performs a linear interpolation between two vec3's
*
* @param {vec3} out the receiving vector
* @param {vec3} a the first operand
* @param {vec3} b the second operand
* @param {Number} t interpolation amount between the two inputs
* @returns {vec3} out
*/
function lerp(out, a, b, t) {
let ax = a[0];
let ay = a[1];
let az = a[2];
out[0] = ax + t * (b[0] - ax);
out[1] = ay + t * (b[1] - ay);
out[2] = az + t * (b[2] - az);
return out;
}
/**
* Transforms the vec3 with a mat4.
* 4th vector component is implicitly '1'
*
* @param {vec3} out the receiving vector
* @param {vec3} a the vector to transform
* @param {mat4} m matrix to transform with
* @returns {vec3} out
*/
function transformMat4(out, a, m) {
let x = a[0],
y = a[1],
z = a[2];
let w = m[3] * x + m[7] * y + m[11] * z + m[15];
w = w || 1.0;
out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
return out;
}
/**
* Same as above but doesn't apply translation.
* Useful for rays.
*/
function scaleRotateMat4(out, a, m) {
let x = a[0],
y = a[1],
z = a[2];
let w = m[3] * x + m[7] * y + m[11] * z + m[15];
w = w || 1.0;
out[0] = (m[0] * x + m[4] * y + m[8] * z) / w;
out[1] = (m[1] * x + m[5] * y + m[9] * z) / w;
out[2] = (m[2] * x + m[6] * y + m[10] * z) / w;
return out;
}
/**
* Transforms the vec3 with a quat
*
* @param {vec3} out the receiving vector
* @param {vec3} a the vector to transform
* @param {quat} q quaternion to transform with
* @returns {vec3} out
*/
function transformQuat(out, a, q) {
// benchmarks: https://jsperf.com/quaternion-transform-vec3-implementations-fixed
let x = a[0],
y = a[1],
z = a[2];
let qx = q[0],
qy = q[1],
qz = q[2],
qw = q[3];
let uvx = qy * z - qz * y;
let uvy = qz * x - qx * z;
let uvz = qx * y - qy * x;
let uuvx = qy * uvz - qz * uvy;
let uuvy = qz * uvx - qx * uvz;
let uuvz = qx * uvy - qy * uvx;
let w2 = qw * 2;
uvx *= w2;
uvy *= w2;
uvz *= w2;
uuvx *= 2;
uuvy *= 2;
uuvz *= 2;
out[0] = x + uvx + uuvx;
out[1] = y + uvy + uuvy;
out[2] = z + uvz + uuvz;
return out;
}
/**
* Get the angle between two 3D vectors
* @param {vec3} a The first operand
* @param {vec3} b The second operand
* @returns {Number} The angle in radians
*/
const angle = (function () {
const tempA = [0, 0, 0];
const tempB = [0, 0, 0];
return function (a, b) {
copy(tempA, a);
copy(tempB, b);
normalize(tempA, tempA);
normalize(tempB, tempB);
let cosine = dot(tempA, tempB);
if (cosine > 1.0) {
return 0;
} else if (cosine < -1.0) {
return Math.PI;
} else {
return Math.acos(cosine);
}
};
})();
/**
* Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===)
*
* @param {vec3} a The first vector.
* @param {vec3} b The second vector.
* @returns {Boolean} True if the vectors are equal, false otherwise.
*/
function exactEquals(a, b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
}
class Vec3 extends Array {
constructor(x = 0, y = x, z = x) {
super(x, y, z);
return this;
}
get x() {
return this[0];
}
get y() {
return this[1];
}
get z() {
return this[2];
}
set x(v) {
this[0] = v;
}
set y(v) {
this[1] = v;
}
set z(v) {
this[2] = v;
}
set(x, y = x, z = x) {
if (x.length) return this.copy(x);
set(this, x, y, z);
return this;
}
copy(v) {
copy(this, v);
return this;
}
add(va, vb) {
if (vb) add(this, va, vb);
else add(this, this, va);
return this;
}
sub(va, vb) {
if (vb) subtract(this, va, vb);
else subtract(this, this, va);
return this;
}
multiply(v) {
if (v.length) multiply(this, this, v);
else scale(this, this, v);
return this;
}
divide(v) {
if (v.length) divide(this, this, v);
else scale(this, this, 1 / v);
return this;
}
inverse(v = this) {
inverse(this, v);
return this;
}
// Can't use 'length' as Array.prototype uses it
len() {
return length(this);
}
distance(v) {
if (v) return distance(this, v);
else return length(this);
}
squaredLen() {
return squaredLength(this);
}
squaredDistance(v) {
if (v) return squaredDistance(this, v);
else return squaredLength(this);
}
negate(v = this) {
negate(this, v);
return this;
}
cross(va, vb) {
if (vb) cross(this, va, vb);
else cross(this, this, va);
return this;
}
scale(v) {
scale(this, this, v);
return this;
}
normalize() {
normalize(this, this);
return this;
}
dot(v) {
return dot(this, v);
}
equals(v) {
return exactEquals(this, v);
}
applyMatrix4(mat4) {
transformMat4(this, this, mat4);
return this;
}
scaleRotateMatrix4(mat4) {
scaleRotateMat4(this, this, mat4);
return this;
}
applyQuaternion(q) {
transformQuat(this, this, q);
return this;
}
angle(v) {
return angle(this, v);
}
lerp(v, t) {
lerp(this, this, v, t);
return this;
}
clone() {
return new Vec3(this[0], this[1], this[2]);
}
fromArray(a, o = 0) {
this[0] = a[o];
this[1] = a[o + 1];
this[2] = a[o + 2];
return this;
}
toArray(a = [], o = 0) {
a[o] = this[0];
a[o + 1] = this[1];
a[o + 2] = this[2];
return a;
}
transformDirection(mat4) {
const x = this[0];
const y = this[1];
const z = this[2];
this[0] = mat4[0] * x + mat4[4] * y + mat4[8] * z;
this[1] = mat4[1] * x + mat4[5] * y + mat4[9] * z;
this[2] = mat4[2] * x + mat4[6] * y + mat4[10] * z;
return this.normalize();
}
}
// attribute params
const tempVec3 = new Vec3();
let ID = 1;
let ATTR_ID = 1;
// To stop inifinite warnings
let isBoundsWarned = false;
class Geometry {
constructor(gl, attributes = {}) {
if (!gl.canvas) console.error('gl not passed as first argument to Geometry');
this.gl = gl;
this.attributes = attributes;
this.id = ID++;
// Store one VAO per program attribute locations order
this.VAOs = {};
this.drawRange = { start: 0, count: 0 };
this.instancedCount = 0;
// Unbind current VAO so that new buffers don't get added to active mesh
this.gl.renderer.bindVertexArray(null);
this.gl.renderer.currentGeometry = null;
// Alias for state store to avoid redundant calls for global state
this.glState = this.gl.renderer.state;
// create the buffers
for (let key in attributes) {
this.addAttribute(key, attributes[key]);
}
}
addAttribute(key, attr) {
this.attributes[key] = attr;
// Set options
attr.id = ATTR_ID++; // TODO: currently unused, remove?
attr.size = attr.size || 1;
attr.type =
attr.type ||
(attr.data.constructor === Float32Array
? this.gl.FLOAT
: attr.data.constructor === Uint16Array
? this.gl.UNSIGNED_SHORT
: this.gl.UNSIGNED_INT); // Uint32Array
attr.target = key === 'index' ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER;
attr.normalized = attr.normalized || false;
attr.stride = attr.stride || 0;
attr.offset = attr.offset || 0;
attr.count = attr.count || (attr.stride ? attr.data.byteLength / attr.stride : attr.data.length / attr.size);
attr.divisor = attr.instanced || 0;
attr.needsUpdate = false;
if (!attr.buffer) {
attr.buffer = this.gl.createBuffer();
// Push data to buffer
this.updateAttribute(attr);
}
// Update geometry counts. If indexed, ignore regular attributes
if (attr.divisor) {
this.isInstanced = true;
if (this.instancedCount && this.instancedCount !== attr.count * attr.divisor) {
console.warn('geometry has multiple instanced buffers of different length');
return (this.instancedCount = Math.min(this.instancedCount, attr.count * attr.divisor));
}
this.instancedCount = attr.count * attr.divisor;
} else if (key === 'index') {
this.drawRange.count = attr.count;
} else if (!this.attributes.index) {
this.drawRange.count = Math.max(this.drawRange.count, attr.count);
}
}
updateAttribute(attr) {
if (this.glState.boundBuffer !== attr.buffer) {
this.gl.bindBuffer(attr.target, attr.buffer);
this.glState.boundBuffer = attr.buffer;
}
this.gl.bufferData(attr.target, attr.data, this.gl.STATIC_DRAW);
attr.needsUpdate = false;
}
setIndex(value) {
this.addAttribute('index', value);
}
setDrawRange(start, count) {
this.drawRange.start = start;
this.drawRange.count = count;
}
setInstancedCount(value) {
this.instancedCount = value;
}
createVAO(program) {
this.VAOs[program.attributeOrder] = this.gl.renderer.createVertexArray();
this.gl.renderer.bindVertexArray(this.VAOs[program.attributeOrder]);
this.bindAttributes(program);
}
bindAttributes(program) {
// Link all attributes to program using gl.vertexAttribPointer
program.attributeLocations.forEach((location, { name, type }) => {
// If geometry missing a required shader attribute
if (!this.attributes[name]) {
console.warn(`active attribute ${name} not being supplied`);
return;
}
const attr = this.attributes[name];
this.gl.bindBuffer(attr.target, attr.buffer);
this.glState.boundBuffer = attr.buffer;
// For matrix attributes, buffer needs to be defined per column
let numLoc = 1;
if (type === 35674) numLoc = 2; // mat2
if (type === 35675) numLoc = 3; // mat3
if (type === 35676) numLoc = 4; // mat4
const size = attr.size / numLoc;
const stride = numLoc === 1 ? 0 : numLoc * numLoc * numLoc;
const offset = numLoc === 1 ? 0 : numLoc * numLoc;
for (let i = 0; i < numLoc; i++) {
this.gl.vertexAttribPointer(location + i, size, attr.type, attr.normalized, attr.stride + stride, attr.offset + i * offset);
this.gl.enableVertexAttribArray(location + i);
// For instanced attributes, divisor needs to be set.
// For firefox, need to set back to 0 if non-instanced drawn after instanced. Else won't render
this.gl.renderer.vertexAttribDivisor(location + i, attr.divisor);
}
});
// Bind indices if geometry indexed
if (this.attributes.index) this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.attributes.index.buffer);
}
draw({ program, mode = this.gl.TRIANGLES }) {
if (this.gl.renderer.currentGeometry !== `${this.id}_${program.attributeOrder}`) {
if (!this.VAOs[program.attributeOrder]) this.createVAO(program);
this.gl.renderer.bindVertexArray(this.VAOs[program.attributeOrder]);
this.gl.renderer.currentGeometry = `${this.id}_${program.attributeOrder}`;
}
// Check if any attributes need updating
program.attributeLocations.forEach((location, { name }) => {
const attr = this.attributes[name];
if (attr.needsUpdate) this.updateAttribute(attr);
});
if (this.isInstanced) {
if (this.attributes.index) {
this.gl.renderer.drawElementsInstanced(
mode,
this.drawRange.count,
this.attributes.index.type,
this.attributes.index.offset + this.drawRange.start * 2,
this.instancedCount
);
} else {
this.gl.renderer.drawArraysInstanced(mode, this.drawRange.start, this.drawRange.count, this.instancedCount);
}
} else {
if (this.attributes.index) {
this.gl.drawElements(mode, this.drawRange.count, this.attributes.index.type, this.attributes.index.offset + this.drawRange.start * 2);
} else {
this.gl.drawArrays(mode, this.drawRange.start, this.drawRange.count);
}
}
}
getPositionArray() {
// Use position buffer, or min/max if available
const attr = this.attributes.position;
// if (attr.min) return [...attr.min, ...attr.max];
if (attr.data) return attr.data;
if (isBoundsWarned) return;
console.warn('No position buffer data found to compute bounds');
return (isBoundsWarned = true);
}
computeBoundingBox(array) {
if (!array) array = this.getPositionArray();
if (!this.bounds) {
this.bounds = {
min: new Vec3(),
max: new Vec3(),
center: new Vec3(),
scale: new Vec3(),
radius: Infinity,
};
}
const min = this.bounds.min;
const max = this.bounds.max;
const center = this.bounds.center;
const scale = this.bounds.scale;
min.set(+Infinity);
max.set(-Infinity);
// TODO: use offset/stride if exists
// TODO: check size of position (eg triangle with Vec2)
for (let i = 0, l = array.length; i < l; i += 3) {
const x = array[i];
const y = array[i + 1];
const z = array[i + 2];
min.x = Math.min(x, min.x);
min.y = Math.min(y, min.y);
min.z = Math.min(z, min.z);
max.x = Math.max(x, max.x);
max.y = Math.max(y, max.y);
max.z = Math.max(z, max.z);
}
scale.sub(max, min);
center.add(min, max).divide(2);
}
computeBoundingSphere(array) {
if (!array) array = this.getPositionArray();
if (!this.bounds) this.computeBoundingBox(array);
let maxRadiusSq = 0;
for (let i = 0, l = array.length; i < l; i += 3) {
tempVec3.fromArray(array, i);
maxRadiusSq = Math.max(maxRadiusSq, this.bounds.center.squaredDistance(tempVec3));
}
this.bounds.radius = Math.sqrt(maxRadiusSq);
}
remove() {
if (this.vao) this.gl.renderer.deleteVertexArray(this.vao);
for (let key in this.attributes) {
this.gl.deleteBuffer(this.attributes[key].buffer);
delete this.attributes[key];
}
}
}
// TODO: upload empty texture if null ? maybe not
// TODO: upload identity matrix if null ?
// TODO: sampler Cube
let ID$1 = 1;
// cache of typed arrays used to flatten uniform arrays
const arrayCacheF32 = {};
class Program {
constructor(
gl,
{
vertex,
fragment,
uniforms = {},
transparent = false,
cullFace = gl.BACK,
frontFace = gl.CCW,
depthTest = true,
depthWrite = true,
depthFunc = gl.LESS,
} = {}
) {
if (!gl.canvas) console.error('gl not passed as fist argument to Program');
this.gl = gl;
this.uniforms = uniforms;
this.id = ID$1++;
if (!vertex) console.warn('vertex shader not supplied');
if (!fragment) console.warn('fragment shader not supplied');
// Store program state
this.transparent = transparent;
this.cullFace = cullFace;
this.frontFace = frontFace;
this.depthTest = depthTest;
this.depthWrite = depthWrite;
this.depthFunc = depthFunc;
this.blendFunc = {};
this.blendEquation = {};
// set default blendFunc if transparent flagged
if (this.transparent && !this.blendFunc.src) {
if (this.gl.renderer.premultipliedAlpha) this.setBlendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
else this.setBlendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
}
// compile vertex shader and log errors
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);
if (gl.getShaderInfoLog(vertexShader) !== '') {
console.warn(`${gl.getShaderInfoLog(vertexShader)}\nVertex Shader\n${addLineNumbers(vertex)}`);
}
// compile fragment shader and log errors
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);
if (gl.getShaderInfoLog(fragmentShader) !== '') {
console.warn(`${gl.getShaderInfoLog(fragmentShader)}\nFragment Shader\n${addLineNumbers(fragment)}`);
}
// compile program and log errors
this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
return console.warn(gl.getProgramInfoLog(this.program));
}
// Remove shader once linked
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
// Get active uniform locations
this.uniformLocations = new Map();
let numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
for (let uIndex = 0; uIndex < numUniforms; uIndex++) {
let uniform = gl.getActiveUniform(this.program, uIndex);
this.uniformLocations.set(uniform, gl.getUniformLocation(this.program, uniform.name));
// split uniforms' names to separate array and struct declarations
const split = uniform.name.match(/(\w+)/g);
uniform.uniformName = split[0];
if (split.length === 3) {
uniform.isStructArray = true;
uniform.structIndex = Number(split[1]);
uniform.structProperty = split[2];
} else if (split.length === 2 && isNaN(Number(split[1]))) {
uniform.isStruct = true;
uniform.structProperty = split[1];
}
}
// Get active attribute locations
this.attributeLocations = new Map();
const locations = [];
const numAttribs = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
for (let aIndex = 0; aIndex < numAttribs; aIndex++) {
const attribute = gl.getActiveAttrib(this.program, aIndex);
const location = gl.getAttribLocation(this.program, attribute.name);
locations[location] = attribute.name;
this.attributeLocations.set(attribute, location);
}
this.attributeOrder = locations.join('');
}
setBlendFunc(src, dst, srcAlpha, dstAlpha) {
this.blendFunc.src = src;
this.blendFunc.dst = dst;
this.blendFunc.srcAlpha = srcAlpha;
this.blendFunc.dstAlpha = dstAlpha;
if (src) this.transparent = true;
}
setBlendEquation(modeRGB, modeAlpha) {
this.blendEquation.modeRGB = modeRGB;
this.blendEquation.modeAlpha = modeAlpha;
}
applyState() {
if (this.depthTest) this.gl.renderer.enable(this.gl.DEPTH_TEST);
else this.gl.renderer.disable(this.gl.DEPTH_TEST);
if (this.cullFace) this.gl.renderer.enable(this.gl.CULL_FACE);
else this.gl.renderer.disable(this.gl.CULL_FACE);
if (this.blendFunc.src) this.gl.renderer.enable(this.gl.BLEND);
else this.gl.renderer.disable(this.gl.BLEND);
if (this.cullFace) this.gl.renderer.setCullFace(this.cullFace);
this.gl.renderer.setFrontFace(this.frontFace);
this.gl.renderer.setDepthMask(this.depthWrite);
this.gl.renderer.setDepthFunc(this.depthFunc);
if (this.blendFunc.src)
this.gl.renderer.setBlendFunc(this.blendFunc.src, this.blendFunc.dst, this.blendFunc.srcAlpha, this.blendFunc.dstAlpha);
this.gl.renderer.setBlendEquation(this.blendEquation.modeRGB, this.blendEquation.modeAlpha);
}
use({ flipFaces = false } = {}) {
let textureUnit = -1;
const programActive = this.gl.renderer.currentProgram === this.id;
// Avoid gl call if program already in use
if (!programActive) {
this.gl.useProgram(this.program);
this.gl.renderer.currentProgram = this.id;
}
// Set only the active uniforms found in the shader
this.uniformLocations.forEach((location, activeUniform) => {
let name = activeUniform.uniformName;
// get supplied uniform
let uniform = this.uniforms[name];
// For structs, get the specific property instead of the entire object
if (activeUniform.isStruct) {
uniform = uniform[activeUniform.structProperty];
name += `.${activeUniform.structProperty}`;
}
if (activeUniform.isStructArray) {
uniform = uniform[activeUniform.structIndex][activeUniform.structProperty];
name += `[${activeUniform.structIndex}].${activeUniform.structProperty}`;
}
if (!uniform) {
return warn(`Active uniform ${name} has not been supplied`);
}
if (uniform && uniform.value === undefined) {
return warn(`${name} uniform is missing a value parameter`);
}
if (uniform.value.texture) {
textureUnit = textureUnit + 1;
// Check if texture needs to be updated
uniform.value.update(textureUnit);
return setUniform(this.gl, activeUniform.type, location, textureUnit);
}
// For texture arrays, set uniform as an array of texture units instead of just one
if (uniform.value.length && uniform.value[0].texture) {
const textureUnits = [];
uniform.value.forEach((value) => {
textureUnit = textureUnit + 1;
value.update(textureUnit);
textureUnits.push(textureUnit);
});
return setUniform(this.gl, activeUniform.type, location, textureUnits);
}
setUniform(this.gl, activeUniform.type, location, uniform.value);
});
this.applyState();
if (flipFaces) this.gl.renderer.setFrontFace(this.frontFace === this.gl.CCW ? this.gl.CW : this.gl.CCW);
}
remove() {
this.gl.deleteProgram(this.program);
}
}
function setUniform(gl, type, location, value) {
value = value.length ? flatten(value) : value;
const setValue = gl.renderer.state.uniformLocations.get(location);
// Avoid redundant uniform commands
if (value.length) {
if (setValue === undefined || setValue.length !== value.length) {
// clone array to store as cache
gl.renderer.state.uniformLocations.set(location, value.slice(0));
} else {
if (arraysEqual(setValue, value)) return;
// Update cached array values
setValue.set ? setValue.set(value) : setArray(setValue, value);
gl.renderer.state.uniformLocations.set(location, setValue);
}
} else {
if (setValue === value) return;
gl.renderer.state.uniformLocations.set(location, value);
}
switch (type) {
case 5126:
return value.length ? gl.uniform1fv(location, value) : gl.uniform1f(location, value); // FLOAT
case 35664:
return gl.uniform2fv(location, value); // FLOAT_VEC2
case 35665:
return gl.uniform3fv(location, value); // FLOAT_VEC3
case 35666:
return gl.uniform4fv(location, value); // FLOAT_VEC4
case 35670: // BOOL
case 5124: // INT
case 35678: // SAMPLER_2D
case 35680:
return value.length ? gl.uniform1iv(location, value) : gl.uniform1i(location, value); // SAMPLER_CUBE
case 35671: // BOOL_VEC2
case 35667:
return gl.uniform2iv(location, value); // INT_VEC2
case 35672: // BOOL_VEC3
case 35668:
return gl.uniform3iv(location, value); // INT_VEC3
case 35673: // BOOL_VEC4
case 35669:
return gl.uniform4iv(location, value); // INT_VEC4
case 35674:
return gl.uniformMatrix2fv(location, false, value); // FLOAT_MAT2
case 35675:
return gl.uniformMatrix3fv(location, false, value); // FLOAT_MAT3
case 35676:
return gl.uniformMatrix4fv(location, false, value); // FLOAT_MAT4
}
}
function addLineNumbers(string) {
let lines = string.split('\n');
for (let i = 0; i < lines.length; i++) {
lines[i] = i + 1 + ': ' + lines[i];
}
return lines.join('\n');
}
function flatten(a) {
const arrayLen = a.length;
const valueLen = a[0].length;
if (valueLen === undefined) return a;
const length = arrayLen * valueLen;
let value = arrayCacheF32[length];
if (!value) arrayCacheF32[length] = value = new Float32Array(length);
for (let i = 0; i < arrayLen; i++) value.set(a[i], i * valueLen);
return value;
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0, l = a.length; i < l; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function setArray(a, b) {
for (let i = 0, l = a.length; i < l; i++) {
a[i] = b[i];
}
}
let warnCount = 0;
function warn(message) {
if (warnCount > 100) return;
console.warn(message);
warnCount++;
if (warnCount > 100) console.warn('More than 100 program warnings - stopping logs.');
}
// TODO: Handle context loss https://www.khronos.org/webgl/wiki/HandlingContextLost
// Not automatic - devs to use these methods manually
// gl.colorMask( colorMask, colorMask, colorMask, colorMask );
// gl.clearColor( r, g, b, a );
// gl.stencilMask( stencilMask );
// gl.stencilFunc( stencilFunc, stencilRef, stencilMask );
// gl.stencilOp( stencilFail, stencilZFail, stencilZPass );
// gl.clearStencil( stencil );
const tempVec3$1 = new Vec3();
let ID$2 = 1;
class Renderer {
constructor({
canvas = document.createElement('canvas'),
width = 300,
height = 150,
dpr = 1,
alpha = false,
depth = true,
stencil = false,
antialias = false,
premultipliedAlpha = false,
preserveDrawingBuffer = false,
powerPreference = 'default',
autoClear = true,
webgl = 2,
} = {}) {
const attributes = { alpha, depth, stencil, antialias, premultipliedAlpha, preserveDrawingBuffer, powerPreference };
this.dpr = dpr;
this.alpha = alpha;
this.color = true;
this.depth = depth;
this.stencil = stencil;
this.premultipliedAlpha = premultipliedAlpha;
this.autoClear = autoClear;
this.id = ID$2++;
// Attempt WebGL2 unless forced to 1, if not supported fallback to WebGL1
if (webgl === 2) this.gl = canvas.getContext('webgl2', attributes);
this.isWebgl2 = !!this.gl;
if (!this.gl) {
this.gl = canvas.getContext('webgl', attributes) || canvas.getContext('experimental-webgl', attributes);
}
// Attach renderer to gl so that all classes have access to internal state functions
this.gl.renderer = this;
// initialise size values
this.setSize(width, height);
// gl state stores to avoid redundant calls on methods used internally
this.state = {};
this.state.blendFunc = { src: this.gl.ONE, dst: this.gl.ZERO };
this.state.blendEquation = { modeRGB: this.gl.FUNC_ADD };
this.state.cullFace = null;
this.state.frontFace = this.gl.CCW;
this.state.depthMask = true;
this.state.depthFunc = this.gl.LESS;
this.state.premultiplyAlpha = false;
this.state.flipY = false;
this.state.unpackAlignment = 4;
this.state.framebuffer = null;
this.state.viewport = { width: null, height: null };
this.state.textureUnits = [];
this.state.activeTextureUnit = 0;
this.state.boundBuffer = null;
this.state.uniformLocations = new Map();
// store requested extensions
this.extensions = {};
// Initialise extra format types
if (this.isWebgl2) {
this.getExtension('EXT_color_buffer_float');
this.getExtension('OES_texture_float_linear');
} else {
this.getExtension('OES_texture_float');
this.getExtension('OES_texture_float_linear');
this.getExtension('OES_texture_half_float');
this.getExtension('OES_texture_half_float_linear');
this.getExtension('OES_element_index_uint');
this.getExtension('OES_standard_derivatives');
this.getExtension('EXT_sRGB');
this.getExtension('WEBGL_depth_texture');
this.getExtension('WEBGL_draw_buffers');
}
// Create method aliases using extension (WebGL1) or native if available (WebGL2)
this.vertexAttribDivisor = this.getExtension('ANGLE_instanced_arrays', 'vertexAttribDivisor', 'vertexAttribDivisorANGLE');
this.drawArraysInstanced = this.getExtension('ANGLE_instanced_arrays', 'drawArraysInstanced', 'drawArraysInstancedANGLE');
this.drawElementsInstanced = this.getExtension('ANGLE_instanced_arrays', 'drawElementsInstanced', 'drawElementsInstancedANGLE');
this.createVertexArray = this.getExtension('OES_vertex_array_object', 'createVertexArray', 'createVertexArrayOES');
this.bindVertexArray = this.getExtension('OES_vertex_array_object', 'bindVertexArray', 'bindVertexArrayOES');
this.deleteVertexArray = this.getExtension('OES_vertex_array_object', 'deleteVertexArray', 'deleteVertexArrayOES');
this.drawBuffers = this.getExtension('WEBGL_draw_buffers', 'drawBuffers', 'drawBuffersWEBGL');
// Store device parameters
this.parameters = {};
this.parameters.maxTextureUnits = this.gl.getParameter(this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
this.parameters.maxAnisotropy = this.getExtension('EXT_texture_filter_anisotropic')
? this.gl.getParameter(this.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT)
: 0;
}
setSize(width, height) {
this.width = width;
this.height = height;
this.gl.canvas.width = width * this.dpr;
this.gl.canvas.height = height * this.dpr;
Object.assign(this.gl.canvas.style, {
width: width + 'px',
height: height + 'px',
});
}
setViewport(width, height) {
if (this.state.viewport.width === width && this.state.viewport.height === height) return;
this.state.viewport.width = width;
this.state.viewport.height = height;
this.gl.viewport(0, 0, width, height);
}
enable(id) {
if (this.state[id] === true) return;
this.gl.enable(id);
this.state[id] = true;
}
disable(id) {
if (this.state[id] === false) return;
this.gl.disable(id);
this.state[id] = false;
}
setBlendFunc(src, dst, srcAlpha, dstAlpha) {
if (
this.state.blendFunc.src === src &&
this.state.blendFunc.dst === dst &&
this.state.blendFunc.srcAlpha === srcAlpha &&
this.state.blendFunc.dstAlpha === dstAlpha
)
return;
this.state.blendFunc.src = src;
this.state.blendFunc.dst = dst;
this.state.blendFunc.srcAlpha = srcAlpha;
this.state.blendFunc.dstAlpha = dstAlpha;
if (srcAlpha !== undefined) this.gl.blendFuncSeparate(src, dst, srcAlpha, dstAlpha);
else this.gl.blendFunc(src, dst);
}
setBlendEquation(modeRGB, modeAlpha) {
modeRGB = modeRGB || this.gl.FUNC_ADD;
if (this.state.blendEquation.modeRGB === modeRGB && this.state.blendEquation.modeAlpha === modeAlpha) return;
this.state.blendEquation.modeRGB = modeRGB;
this.state.blendEquation.modeAlpha = modeAlpha;
if (modeAlpha !== undefined) this.gl.blendEquationSeparate(modeRGB, modeAlpha);
else this.gl.blendEquation(modeRGB);
}
setCullFace(value) {
if (this.state.cullFace === value) return;
this.state.cullFace = value;
this.gl.cullFace(value);
}
setFrontFace(value) {
if (this.state.frontFace === value) return;
this.state.frontFace = value;
this.gl.frontFace(value);
}
setDepthMask(value) {
if (this.state.depthMask === value) return;
this.state.depthMask = value;
this.gl.depthMask(value);
}
setDepthFunc(value) {
if (this.state.depthFunc === value) return;
this.state.depthFunc = value;
this.gl.depthFunc(value);
}
activeTexture(value) {
if (this.state.activeTextureUnit === value) return;
this.state.activeTextureUnit = value;
this.gl.activeTexture(this.gl.TEXTURE0 + value);
}
bindFramebuffer({ target = this.gl.FRAMEBUFFER, buffer = null } = {}) {
if (this.state.framebuffer === buffer) return;
this.state.framebuffer = buffer;
this.gl.bindFramebuffer(target, buffer);
}
getExtension(extension, webgl2Func, extFunc) {
// if webgl2 function supported, return func bound to gl context
if (webgl2Func && this.gl[webgl2Func]) return this.gl[webgl2Func].bind(this.gl);
// fetch extension once only
if (!this.extensions[extension]) {
this.extensions[extension] = this.gl.getExtension(extension);
}
// return extension if no function requested
if (!webgl2Func) return this.extensions[extension];
// Return null if extension not supported
if (!this.extensions[extension]) return null;
// return extension function, bound to extension
return this.extensions[extension][extFunc].bind(this.extensions[extension]);
}
sortOpaque(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
} else if (a.program.id !== b.program.id) {
return a.program.id - b.program.id;
} else if (a.zDepth !== b.zDepth) {
return a.zDepth - b.zDepth;
} else {
return b.id - a.id;
}
}
sortTransparent(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
}
if (a.zDepth !== b.zDepth) {
return b.zDepth - a.zDepth;
} else {
return b.id - a.id;
}
}
sortUI(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
} else if (a.program.id !== b.program.id) {
return a.program.id - b.program.id;
} else {
return b.id - a.id;
}
}
getRenderList({ scene, camera, frustumCull, sort }) {
let renderList = [];
if (camera && frustumCull) camera.updateFrustum();
// Get visible
scene.traverse((node) => {
if (!node.visible) return true;
if (!node.draw) return;
if (frustumCull && node.frustumCulled && camera) {
if (!camera.frustumIntersectsMesh(node)) return;
}
renderList.push(node);
});
if (sort) {
const opaque = [];
const transparent = []; // depthTest true
const ui = []; // depthTest false
renderList.forEach((node) => {
// Split into the 3 render groups
if (!node.program.transparent) {
opaque.push(node);
} else if (node.program.depthTest) {
transparent.push(node);
} else {
ui.push(node);
}
node.zDepth = 0;
// Only calculate z-depth if renderOrder unset and depthTest is true
if (node.renderOrder !== 0 || !node.program.depthTest || !camera) return;
// update z-depth
node.worldMatrix.getTranslation(tempVec3$1);
tempVec3$1.applyMatrix4(camera.projectionViewMatrix);
node.zDepth = tempVec3$1.z;
});
opaque.sort(this.sortOpaque);
transparent.sort(this.sortTransparent);
ui.sort(this.sortUI);
renderList = opaque.concat(transparent, ui);
}
return renderList;
}
render({ scene, camera, target = null, update = true, sort = true, frustumCull = true, clear }) {
if (target === null) {
// make sure no render target bound so draws to canvas
this.bindFramebuffer();
this.setViewport(this.width * this.dpr, this.height * this.dpr);
} else {
// bind supplied render target and update viewport
this.bindFramebuffer(target);
this.setViewport(target.width, target.height);
}
if (clear || (this.autoClear && clear !== false)) {
// Ensure depth buffer writing is enabled so it can be cleared
if (this.depth && (!target || target.depth)) {
this.enable(this.gl.DEPTH_TEST);
this.setDepthMask(true);
}
this.gl.clear(
(this.color ? this.gl.COLOR_BUFFER_BIT : 0) |
(this.depth ? this.gl.DEPTH_BUFFER_BIT : 0) |
(this.stencil ? this.gl.STENCIL_BUFFER_BIT : 0)
);
}
// updates all scene graph matrices
if (update) scene.updateMatrixWorld();
// Update camera separately, in case not in scene graph
if (camera) camera.updateMatrixWorld();
// Get render list - entails culling and sorting
const renderList = this.getRenderList({ scene, camera, frustumCull, sort });
renderList.forEach((node) => {
node.draw({ camera });
});
}
}
/**
* Copy the values from one vec4 to another
*
* @param {vec4} out the receiving vector
* @param {vec4} a the source vector
* @returns {vec4} out
*/
function copy$1(out, a) {
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
out[3] = a[3];
return out;
}
/**
*