UNPKG

ogl

Version:
1,477 lines (1,264 loc) 280 kB
(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; } /** *