├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── assets └── screenshot.gif ├── dist └── mini-webgl.js ├── examples ├── complex.html ├── cube.html ├── materials.html └── simple.html ├── package.json ├── src ├── buffer-attribute.js ├── camera.js ├── geometries │ ├── cube.js │ ├── geometry.js │ └── triangle.js ├── index.js ├── materials │ ├── basic-material.js │ └── material.js ├── math.js ├── model.js ├── node.js ├── renderer.js ├── scene.js ├── shaders │ ├── base.fs │ ├── base.vs │ ├── basic.fs │ └── basic.vs └── webgl │ ├── attribute.js │ ├── gl-buffers.js │ ├── index.js │ ├── program.js │ ├── programs.js │ ├── register.js │ └── uniform.js ├── vendor └── webgl-debug.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "es2016", 5 | "es2017", 6 | ], 7 | "plugins": ["transform-object-rest-spread"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sw* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jordan Santell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-webgl 2 | 3 | Mini toy WebGL library 4 | 5 | For educational purposes. **Do not use this library.** 6 | This is a barebones, unoptimized abstraction around WebGL. API ideas inspired by THREE.js. 7 | 8 | ![mini-webgl](https://jsantell.github.io/mini-webgl/assets/screenshot.gif) 9 | 10 | ## What and How it Works 11 | 12 | * Basic rendering of a [scene](src/scene.js) graph with [perspective camera](src/camera.js). 13 | * [Models](src/model.js) are represented by a geometry and material, updating position/rotation/scale when local coordinates change. 14 | * Barebones [Math](src/math.js) library for managing Vector and Matrix types and track if dirty to minimize rebuilding matrices. Mostly a wrapper around [glMatrix](http://glmatrix.net/). 15 | * [Geometries](src/geometries) have a vertex buffer, and optionally an indices buffer. 16 | * Implemented: [Triangle](src/geometries/triangle.js), [Cube](src/geometries/cube.js) 17 | * [Materials](src/materials) take a vertex and fragment shader and define `uniforms` and `attributes` properties that can be updated and pushed to the GPU. Uniforms/attribs are initialized by readingInstances have their own uniform/attribute values, but reuse the same program if it can (based on vertex/fragment shader source), so for example, all instances of `BasicMaterial` use the same underlying WebGLProgram. 18 | * Implemented: [BasicMaterial](src/materials/basic-material.js) 19 | * The scene's [renderer](src/renderer.js) queues up the scene graph's nodes and passes it to the [GLWrapper](src/webgl/index.js) which manages all the WebGL calls, so all the API abstractions of models and materials are in [src/](src/), and translating that abstraction to WebGL lives in [src/webgl/](src/webgl/). 20 | 21 | ## Example 22 | 23 | * [triangle animations](https://jsantell.github.io/mini-webgl/examples/complex.html) 24 | 25 | ### Example Code 26 | 27 | ```js 28 | var scene = new MiniWebGL.Scene(canvas); 29 | var camera = new MiniWebGL.Camera(); 30 | var cube = new MiniWebGL.Model( 31 | new MiniWebGL.Cube(), 32 | new MiniWebGL.BasicMaterial() 33 | ); 34 | 35 | scene.useCamera(camera); 36 | cube.position.set(0, 0.5, -3); 37 | cube.scale.set(0.5, 0.5, 0.5); 38 | scene.add(cube); 39 | 40 | var t, x, y, z = 0; 41 | function tick () { 42 | t = performance.now(); 43 | x = Math.cos(t * 0.001) + Math.PI; 44 | y = Math.sin(t * 0.001) + Math.PI; 45 | cube.rotation.set(x, y, z); 46 | cube.material.uniforms.color.setX(Math.abs(Math.sin(t * 0.001))); 47 | scene.render(); 48 | requestAnimationFrame(tick); 49 | } 50 | tick(); 51 | ``` 52 | 53 | ## License 54 | 55 | MIT License, Copyright (c) 2017 Jordan Santell 56 | -------------------------------------------------------------------------------- /assets/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsantell/mini-webgl/916099cd5a49f5016ddb80caf76e55f5874fc1d6/assets/screenshot.gif -------------------------------------------------------------------------------- /examples/complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /examples/cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /examples/materials.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsantell/mini-webgl", 3 | "version": "1.0.4", 4 | "description": "Mini toy WebGL library", 5 | "main": "dist/mini-webgl.js", 6 | "scripts": { 7 | "build": "webpack" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jsantell/mini-webgl.git" 12 | }, 13 | "author": "Jordan Santell ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel-core": "^6.18.2", 17 | "babel-loader": "^6.2.8", 18 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 19 | "babel-preset-es2015": "^6.18.0", 20 | "babel-preset-es2016": "^6.16.0", 21 | "babel-preset-es2017": "^6.16.0", 22 | "glslify-loader": "^1.0.2", 23 | "raw-loader": "^0.5.1", 24 | "webpack": "^2.5.0" 25 | }, 26 | "dependencies": { 27 | "gl-matrix": "^2.3.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/buffer-attribute.js: -------------------------------------------------------------------------------- 1 | const ARRAY_BUFFER = 0x8892; 2 | const ELEMENT_ARRAY_BUFFER = 0x8893; 3 | 4 | export default class BufferAttribute { 5 | constructor(array, size, isElementArray) { 6 | this.array = array; 7 | this.size = size; 8 | this.count = array.length / size; 9 | this.isElementArray = isElementArray; 10 | } 11 | 12 | getArray() { 13 | return this.array; 14 | } 15 | 16 | getCount() { 17 | return this.count; 18 | } 19 | 20 | getSize() { 21 | return this.size; 22 | } 23 | 24 | getType() { 25 | return this.isElementArray ? ELEMENT_ARRAY_BUFFER : ARRAY_BUFFER; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/camera.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import { Matrix4 } from './math'; 3 | const PMATRIX = Symbol('pmatrix'); 4 | const IMATRIX = Symbol('imatrix'); 5 | 6 | export default class Camera extends Node { 7 | constructor(fov=45, aspect=1, near=0.01, far=1000) { 8 | super(); 9 | this[PMATRIX] = new Matrix4(); 10 | this[IMATRIX] = new Matrix4(); 11 | this.fov = fov; 12 | this.aspect = aspect; 13 | this.near = near; 14 | this.far = far; 15 | 16 | this.updateProjectionMatrix(); 17 | } 18 | 19 | getProjectionMatrix() { 20 | return this[PMATRIX]; 21 | } 22 | 23 | getInverseWorldMatrix() { 24 | return Matrix4.invert(this[IMATRIX].identity(), this.getWorldMatrix()); 25 | } 26 | 27 | updateProjectionMatrix() { 28 | return this[PMATRIX].perspective(this.fov, this.aspect, this.near, this.far); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/geometries/cube.js: -------------------------------------------------------------------------------- 1 | import Geometry from './geometry'; 2 | import BufferAttribute from '../buffer-attribute'; 3 | 4 | export default class Cube extends Geometry { 5 | constructor() { 6 | const vertices = new BufferAttribute(new Float32Array([ 7 | // Front face 8 | -1.0, -1.0, 1.0, 9 | 1.0, -1.0, 1.0, 10 | 1.0, 1.0, 1.0, 11 | -1.0, 1.0, 1.0, 12 | 13 | // Back face 14 | -1.0, -1.0, -1.0, 15 | -1.0, 1.0, -1.0, 16 | 1.0, 1.0, -1.0, 17 | 1.0, -1.0, -1.0, 18 | 19 | // Top face 20 | -1.0, 1.0, -1.0, 21 | -1.0, 1.0, 1.0, 22 | 1.0, 1.0, 1.0, 23 | 1.0, 1.0, -1.0, 24 | 25 | // Bottom face 26 | -1.0, -1.0, -1.0, 27 | 1.0, -1.0, -1.0, 28 | 1.0, -1.0, 1.0, 29 | -1.0, -1.0, 1.0, 30 | 31 | // Right face 32 | 1.0, -1.0, -1.0, 33 | 1.0, 1.0, -1.0, 34 | 1.0, 1.0, 1.0, 35 | 1.0, -1.0, 1.0, 36 | 37 | // Left face 38 | -1.0, -1.0, -1.0, 39 | -1.0, -1.0, 1.0, 40 | -1.0, 1.0, 1.0, 41 | -1.0, 1.0, -1.0 42 | ]), 3); 43 | 44 | const indices = new BufferAttribute(new Uint16Array([ 45 | 0, 1, 2, 0, 2, 3, // Front face 46 | 4, 5, 6, 4, 6, 7, // Back face 47 | 8, 9, 10, 8, 10, 11, // Top face 48 | 12, 13, 14, 12, 14, 15, // Bottom face 49 | 16, 17, 18, 16, 18, 19, // Right face 50 | 20, 21, 22, 20, 22, 23 // Left face 51 | ]), 1, true); 52 | super(vertices, indices); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/geometries/geometry.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export default class Geometry { 4 | constructor(vertices, indices) { 5 | this.vertices = vertices; 6 | this.indices = indices; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/geometries/triangle.js: -------------------------------------------------------------------------------- 1 | import Geometry from './geometry'; 2 | import BufferAttribute from '../buffer-attribute'; 3 | 4 | export default class Triangle extends Geometry { 5 | constructor() { 6 | const vertices = new BufferAttribute(new Float32Array([ 7 | 0, 1, 0, 8 | -1, -1, 0, 9 | 1, -1, 0 10 | ]), 3); 11 | 12 | super(vertices); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Renderer from './renderer'; 3 | import Camera from './camera'; 4 | import Scene from './scene'; 5 | import * as Math from './math'; 6 | import Model from './model'; 7 | import BasicMaterial from './materials/basic-material'; 8 | import Triangle from './geometries/triangle'; 9 | import Node from './node'; 10 | import Cube from './geometries/cube'; 11 | 12 | export { 13 | assert, 14 | Math, 15 | Renderer, 16 | Camera, 17 | Cube, 18 | Scene, 19 | Model, 20 | Node, 21 | Triangle, 22 | BasicMaterial, 23 | }; 24 | -------------------------------------------------------------------------------- /src/materials/basic-material.js: -------------------------------------------------------------------------------- 1 | import { Vector4 } from '../math'; 2 | import Material from './material'; 3 | import vs from '../shaders/basic.vs'; 4 | import fs from '../shaders/basic.fs'; 5 | 6 | const createDefaultAttributes = () => { 7 | return {}; 8 | }; 9 | 10 | // Create a new instance of the underlying Vector4 data 11 | // so materials can have unique uniforms, or shared if 12 | // supplying its own uniform Vector. 13 | const createDefaultUniforms = () => { 14 | return { 15 | color: new Vector4(1, 1, 1, 1), 16 | } 17 | }; 18 | 19 | export default class BasicMaterial extends Material { 20 | constructor(uniforms={}, attributes={}) { 21 | super(vs, 22 | fs, 23 | // TODO lazily create defaults if needed 24 | Object.assign({}, createDefaultUniforms(), uniforms), 25 | Object.assign({}, createDefaultAttributes(), attributes) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/materials/material.js: -------------------------------------------------------------------------------- 1 | import vertShaderBase from '../shaders/base.vs'; 2 | import fragShaderBase from '../shaders/base.fs'; 3 | 4 | export const standardUniforms = Object.freeze({ 5 | viewMatrix: { type: '4fv' }, 6 | 7 | modelMatrix: { type: '4fv' }, 8 | modelViewMatrix: { type: '4fv' }, 9 | projectionMatrix: { type: '4fv' }, 10 | }); 11 | 12 | export const standardAttribs = Object.freeze({ 13 | position: { type: '3f' }, 14 | }); 15 | 16 | export default class Material { 17 | constructor(vertSrc, fragSrc, uniforms={}, attributes={}) { 18 | this.uniforms = Object.assign({}, uniforms); 19 | this.attributes = Object.assign({}, attributes); 20 | this.vertSrc = vertSrc; 21 | this.fragSrc = fragSrc; 22 | this.fullVertSrc = `${vertShaderBase}\n${this.vertSrc}` 23 | this.fullFragSrc = `${fragShaderBase}\n${this.fragSrc}` 24 | } 25 | 26 | getFragmentSource() { 27 | return this.fullFragSrc; 28 | } 29 | 30 | getVertexSource() { 31 | return this.fullVertSrc; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import glMatrix from 'gl-matrix'; 3 | const { 4 | vec2, 5 | vec3, 6 | vec4, 7 | mat2, 8 | mat3, 9 | mat4, 10 | } = glMatrix; 11 | 12 | const X_SCALAR = [1, 0, 0]; 13 | const Y_SCALAR = [0, 1, 0]; 14 | const Z_SCALAR = [0, 0, 1]; 15 | 16 | class Vector { 17 | constructor(size, x, y, w, z) { 18 | this.size = size; 19 | switch (this.size) { 20 | case 2: 21 | this.mathBase = vec2; 22 | break; 23 | case 3: 24 | this.mathBase = vec3; 25 | break; 26 | case 4: 27 | this.mathBase = vec4; 28 | break; 29 | default: 30 | throw new Error('unknown size'); 31 | } 32 | this.data = this.mathBase.create(); 33 | this.set(x, y, w, z); 34 | this.dirty = true; 35 | this.isVector = true; 36 | } 37 | 38 | getArray() { 39 | return this.data; 40 | } 41 | 42 | identity() { 43 | this.mathBase.identity(this.data); 44 | return this; 45 | } 46 | 47 | set(x, y, z, w) { 48 | if (x !== undefined) this._setIndex(0, x); 49 | if (y !== undefined) this._setIndex(1, y); 50 | if (z !== undefined) this._setIndex(2, z); 51 | if (w !== undefined) this._setIndex(3, w); 52 | } 53 | 54 | getX() { return this._getIndex(0); } 55 | getY() { return this._getIndex(1); } 56 | getZ() { return this._getIndex(2); } 57 | getW() { return this._getIndex(3); } 58 | 59 | setX(v) { return this._setIndex(0, v); } 60 | setY(v) { return this._setIndex(1, v); } 61 | setZ(v) { return this._setIndex(2, v); } 62 | setW(v) { return this._setIndex(3, v); } 63 | 64 | _getIndex(i) { 65 | assert(this.size > i); 66 | return this.data[i]; 67 | } 68 | 69 | _setIndex(i, value) { 70 | assert(this.size > i); 71 | this.data[i] = value; 72 | this.dirty = true; 73 | } 74 | } 75 | 76 | export class Vector2 extends Vector { 77 | constructor(x, y) { 78 | super(2, x, y); 79 | } 80 | } 81 | 82 | export class Vector3 extends Vector { 83 | constructor(x, y, z) { 84 | super(3, x, y, z); 85 | } 86 | } 87 | 88 | export class Vector4 extends Vector { 89 | constructor(x, y, z, w) { 90 | super(4, x, y, z, w); 91 | } 92 | } 93 | 94 | export class Matrix { 95 | constructor(size) { 96 | this.size = size; 97 | switch (this.size) { 98 | case 2: 99 | this.mathBase = mat2; 100 | break; 101 | case 3: 102 | this.mathBase = mat3; 103 | break; 104 | case 4: 105 | this.mathBase = mat4 106 | break; 107 | default: 108 | throw new Error('unknown size'); 109 | } 110 | 111 | this.data = this.mathBase.create(); 112 | this.isMatrix = true; 113 | } 114 | 115 | getArray() { 116 | return this.data; 117 | } 118 | 119 | identity() { 120 | this.mathBase.identity(this.data); 121 | return this; 122 | } 123 | 124 | translate(vec3) { 125 | this.mathBase.translate(this.data, this.data, vec3.getArray()); 126 | } 127 | 128 | rotate(vec3) { 129 | const rArray = vec3.getArray(); 130 | this.mathBase.rotate(this.data, this.data, rArray[0], X_SCALAR); 131 | this.mathBase.rotate(this.data, this.data, rArray[1], Y_SCALAR); 132 | this.mathBase.rotate(this.data, this.data, rArray[2], Z_SCALAR); 133 | } 134 | 135 | scale(vec3) { 136 | this.mathBase.scale(this.data, this.data, vec3.getArray()); 137 | } 138 | } 139 | 140 | export class Matrix2 extends Matrix { 141 | constructor() { 142 | super(2); 143 | } 144 | } 145 | 146 | export class Matrix3 extends Matrix { 147 | constructor() { 148 | super(3); 149 | } 150 | } 151 | 152 | export class Matrix4 extends Matrix { 153 | constructor() { 154 | super(4); 155 | } 156 | 157 | static multiply(out, a, b) { 158 | mat4.multiply(out.getArray(), a.getArray(), b.getArray()); 159 | return out; 160 | } 161 | 162 | static invert(out, a) { 163 | mat4.invert(out.getArray(), a.getArray()); 164 | return out; 165 | } 166 | 167 | perspective(fov, aspect, near, far) { 168 | mat4.perspective(this.data, fov, aspect, near, far); 169 | return this; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | 3 | export default class Model extends Node { 4 | constructor(geometry, material) { 5 | super(); 6 | this.geometry = geometry; 7 | this.material = material; 8 | this.isModel = true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Matrix4, Vector3 } from './math'; 3 | const CHILDREN = Symbol('children'); 4 | const LMATRIX = Symbol('lmatrix'); 5 | const WMATRIX = Symbol('wmatrix'); 6 | const PARENT = Symbol('parent'); 7 | let id = 0; 8 | 9 | /** 10 | * Node is a base class object that can contain children, 11 | * and have `position`, `rotation`, and `scale` properties. 12 | */ 13 | export default class Node { 14 | constructor() { 15 | this.id = ++id; 16 | this[CHILDREN] = new Set(); 17 | 18 | this.position = new Vector3(0, 0, 0); 19 | this.rotation = new Vector3(0, 0, 0); 20 | this.scale = new Vector3(1, 1, 1); 21 | this[LMATRIX] = new Matrix4(); 22 | this[WMATRIX] = new Matrix4(); 23 | this[PARENT] = null; 24 | } 25 | 26 | /** 27 | * Get the model matrix of this node in world coordinates. 28 | * If this node has no parent in the scene graph, then the 29 | * world matrix is the same as the local matrix. 30 | */ 31 | getWorldMatrix() { 32 | if (this[PARENT] === null) { 33 | return this.getLocalMatrix(); 34 | } 35 | 36 | Matrix4.multiply(this[WMATRIX].identity(), 37 | this[PARENT].getWorldMatrix(), 38 | this.getLocalMatrix() 39 | ); 40 | 41 | return this[WMATRIX]; 42 | } 43 | 44 | getLocalMatrix() { 45 | if (this.position.dirty || 46 | this.rotation.dirty || 47 | this.scale.dirty) { 48 | this._updateLocalMatrix(); 49 | } 50 | return this[LMATRIX]; 51 | } 52 | 53 | _updateLocalMatrix() { 54 | const matrix = this[LMATRIX]; 55 | matrix.identity(); 56 | matrix.translate(this.position); 57 | matrix.rotate(this.rotation); 58 | matrix.scale(this.scale); 59 | this.position.dirty = 60 | this.rotation.dirty = 61 | this.scale.dirty = false; 62 | return this; 63 | } 64 | 65 | add(object) { 66 | assert(object instanceof Node, 'Only Nodes may be added to scene graph'); 67 | object[PARENT] = this; 68 | this[CHILDREN].add(object); 69 | } 70 | 71 | remove(object) { 72 | assert(object instanceof Node, 'Only Nodes may be removed from the scene graph'); 73 | object[PARENT] = null; 74 | this[CHILDREN].delete(object); 75 | } 76 | 77 | getChildren() { 78 | return this[CHILDREN].values(); 79 | } 80 | 81 | hasChildren() { 82 | return this[CHILDREN].size !== 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import { Matrix4 } from './math'; 2 | import GLWrapper from './webgl'; 3 | 4 | export default class Renderer { 5 | constructor(canvas) { 6 | let ctx = this.ctx = canvas.getContext('webgl'); 7 | 8 | // Use debugging utils if included 9 | // https://www.khronos.org/webgl/wiki/Debugging 10 | if (false && window.WebGLDebugUtils) { 11 | ctx = WebGLDebugUtils.makeDebugContext( 12 | ctx, 13 | undefined, 14 | (funcName, args) => { 15 | // Log on any undefined args 16 | let valid = true; 17 | for (let i = 0; i < args.length; i++) { 18 | if (args[i] === undefined) { 19 | valid = false; 20 | } 21 | } 22 | if (!valid) { 23 | console.error(`Undefined argument in ${funcName}: ${args}`); 24 | } 25 | } 26 | ); 27 | } 28 | 29 | this.gl = new GLWrapper(ctx); 30 | this.gl.setSize(canvas.width, canvas.height); 31 | 32 | // Temp matrices during render 33 | this._mvMatrix = new Matrix4(); 34 | this._mMatrix = new Matrix4(); 35 | } 36 | 37 | getContext() { 38 | return this.ctx; 39 | } 40 | 41 | setSize(width, height) { 42 | this.gl.setSize(width, height); 43 | } 44 | 45 | render(scene, camera) { 46 | const { gl } = this; 47 | const pMatrix = camera.getProjectionMatrix(); 48 | const vMatrix = camera.getInverseWorldMatrix(); 49 | 50 | // Clear out the canvas 51 | gl.clear(); 52 | 53 | const queue = [scene]; 54 | 55 | while (queue.length) { 56 | const node = queue.shift(); 57 | 58 | // Get and construct our model and model view matrices 59 | const mMatrix = node.getWorldMatrix(); 60 | let mvMatrix = this._mvMatrix; 61 | Matrix4.multiply(mvMatrix.identity(), vMatrix, mMatrix); 62 | 63 | // If we have a model, pass it to the GLWrapper 64 | // to draw 65 | if (node.isModel) { 66 | this.gl.draw(node.geometry, node.material, mMatrix, mvMatrix, pMatrix, vMatrix); 67 | } 68 | 69 | // If this node has children, push them into the queue 70 | if (node.hasChildren()) { 71 | const children = node.getChildren(); 72 | for (let child of children) { 73 | queue.push(child); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scene.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import Camera from './camera'; 3 | import Renderer from './renderer'; 4 | 5 | export default class Scene extends Node { 6 | constructor(canvas) { 7 | super(); 8 | this.camera = null; 9 | this.renderer = new Renderer(canvas); 10 | } 11 | 12 | useCamera(camera) { 13 | this.camera = camera; 14 | } 15 | 16 | render() { 17 | this.renderer.render(this, this.camera); 18 | } 19 | 20 | setSize(width, height) { 21 | this.renderer.setSize(width, height); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/shaders/base.fs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | // uniform mat4 viewMatrix; 4 | // uniform vec3 cameraPosition; 5 | -------------------------------------------------------------------------------- /src/shaders/base.vs: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform mat4 modelMatrix; 4 | uniform mat4 modelViewMatrix; 5 | uniform mat4 projectionMatrix; 6 | uniform mat4 viewMatrix; 7 | // uniform mat3 normalMatrix; 8 | // uniform vec3 cameraPosition; 9 | 10 | attribute vec3 position; 11 | // attribute vec3 normal; 12 | // attribute vec2 uv; 13 | -------------------------------------------------------------------------------- /src/shaders/basic.fs: -------------------------------------------------------------------------------- 1 | uniform vec4 color; 2 | 3 | void main(void) { 4 | gl_FragColor = color; 5 | } 6 | -------------------------------------------------------------------------------- /src/shaders/basic.vs: -------------------------------------------------------------------------------- 1 | void main(void) { 2 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 3 | } 4 | -------------------------------------------------------------------------------- /src/webgl/attribute.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Register from './register'; 3 | 4 | export default class Attribute extends Register { 5 | constructor(gl, program, info) { 6 | super(gl, program, info); 7 | 8 | this.gl.enableVertexAttribArray(this.loc); 9 | } 10 | 11 | _getLocation() { 12 | return this.gl.getAttribLocation(this.program, this.name); 13 | } 14 | 15 | set(value, size) { 16 | const { gl, loc, type } = this; 17 | 18 | gl.bindBuffer(gl.ARRAY_BUFFER, value); 19 | gl.vertexAttribPointer(this.loc, size, gl.FLOAT, false, 0, 0); 20 | this.value = value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/webgl/gl-buffers.js: -------------------------------------------------------------------------------- 1 | const BUFFERS = Symbol('buffers'); 2 | 3 | export default class GLBuffers { 4 | constructor(gl) { 5 | this.gl = gl; 6 | this[BUFFERS] = new Map(); 7 | } 8 | 9 | _initBuffer(array, type) { 10 | const gl = this.gl; 11 | const buffer = gl.createBuffer(); 12 | gl.bindBuffer(type, buffer); 13 | gl.bufferData(type, array, gl.STATIC_DRAW); 14 | this[BUFFERS].set(array, buffer); 15 | return buffer; 16 | } 17 | 18 | /** 19 | * @param {BufferAttribute} bufferAttr 20 | */ 21 | getBuffer(bufferAttr) { 22 | const array = bufferAttr.getArray(); 23 | let buffer = this[BUFFERS].get(array); 24 | if (buffer === undefined) { 25 | buffer = this._initBuffer(array, bufferAttr.getType()); 26 | } 27 | return buffer; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/webgl/index.js: -------------------------------------------------------------------------------- 1 | import Programs from './programs'; 2 | import GLBuffers from './gl-buffers'; 3 | 4 | export default class GLWrapper { 5 | constructor(gl, width, height) { 6 | this.gl = gl; 7 | this.buffers = new GLBuffers(gl); 8 | this.programs = new Programs(gl); 9 | this.width = width; 10 | this.height = height; 11 | } 12 | 13 | setSize(width, height) { 14 | this.width = width; 15 | this.height = height; 16 | } 17 | 18 | clear() { 19 | const { gl } = this; 20 | 21 | gl.clearColor(0.2, 0.2, 0.2, 1); 22 | gl.enable(gl.DEPTH_TEST); 23 | gl.viewport(0, 0, this.width, this.height); 24 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 25 | } 26 | 27 | /** 28 | * @param {Geometry} geometry 29 | * @param {Material} material 30 | * @param {Matrix4} mMatrix 31 | * @param {Matrix4} mvMatrix 32 | * @param {Matrix4} pMatrix 33 | * @param {Matrix4} vMatrix 34 | */ 35 | draw(geometry, material, mMatrix, mvMatrix, pMatrix, vMatrix) { 36 | const gl = this.gl; 37 | const geoVertexBuffer = this.buffers.getBuffer(geometry.vertices); 38 | let geoIndicesBuffer; 39 | if (geometry.indices) { 40 | geoIndicesBuffer = this.buffers.getBuffer(geometry.indices); 41 | } 42 | 43 | // Get the Program abstraction associated with this material, 44 | // or create one, which handles the compiling/linking of 45 | // shaders and setting up of uniforms. Reuses programs even if Material 46 | // instances are not identical based off of frag/vert source. 47 | let program = this.programs.getProgram(material); 48 | 49 | // Start using this program for all future calls 50 | program.use(); 51 | 52 | for (let uniformName of Object.keys(program.uniforms)) { 53 | const uniform = program.uniforms[uniformName]; 54 | 55 | // If this uniform is one of the standard uniforms, 56 | // set it; otherwise, it's probably user-driven 57 | switch (uniform.name) { 58 | case 'modelMatrix': 59 | uniform.set(mMatrix); 60 | break; 61 | case 'modelViewMatrix': 62 | uniform.set(mvMatrix); 63 | break; 64 | case 'projectionMatrix': 65 | uniform.set(pMatrix); 66 | break; 67 | case 'viewMatrix': 68 | uniform.set(vMatrix); 69 | break; 70 | default: 71 | const value = material.uniforms[uniform.name]; 72 | uniform.set(value); 73 | break; 74 | } 75 | } 76 | 77 | for (let attribName of Object.keys(program.attributes)) { 78 | const attribute = program.attributes[attribName]; 79 | 80 | // If this attribute is one of the standard attributes, 81 | // set it; otherwise, it's probably user-driven 82 | switch (attribute.name) { 83 | case 'position': 84 | attribute.set(geoVertexBuffer, geometry.vertices.getSize()); 85 | break; 86 | default: 87 | const bufferAttr = material.attributes[attribute.name]; 88 | const buffer = this.buffers.getBuffer(bufferAttr); 89 | attribute.set(buffer, bufferAttr.getSize()); 90 | break; 91 | } 92 | } 93 | 94 | if (geoIndicesBuffer) { 95 | this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, geoIndicesBuffer); 96 | this.gl.drawElements(this.gl.TRIANGLES, geometry.indices.getCount(), gl.UNSIGNED_SHORT, 0); 97 | } else { 98 | this.gl.drawArrays(this.gl.TRIANGLES, 0, geometry.vertices.getCount()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/webgl/program.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Uniform from './uniform'; 3 | import Attribute from './attribute'; 4 | 5 | const PROGRAM = Symbol('program'); 6 | const VS = Symbol('vs'); 7 | const FS = Symbol('fs'); 8 | 9 | export default class Program { 10 | /** 11 | * @param {WebGLRenderingContext} gl 12 | * @param {String} vertSrc 13 | * @param {String} fragSrc 14 | */ 15 | constructor(gl, vertSrc, fragSrc) { 16 | this.gl = gl; 17 | 18 | this[PROGRAM] = gl.createProgram(); 19 | 20 | this.vertSrc = vertSrc; 21 | this.fragSrc = fragSrc; 22 | 23 | this[VS] = this._createShader(vertSrc, gl.VERTEX_SHADER); 24 | this[FS] = this._createShader(fragSrc, gl.FRAGMENT_SHADER); 25 | 26 | this._attachAndLink(); 27 | 28 | this.attributes = {}; 29 | this.uniforms = {}; 30 | 31 | this._initializeAttributes(); 32 | this._initializeUniforms(); 33 | } 34 | 35 | use() { 36 | this.gl.useProgram(this[PROGRAM]); 37 | } 38 | 39 | getVertexSource() { 40 | return this.vertSrc; 41 | } 42 | 43 | getFragmentSource() { 44 | return this.fragSrc; 45 | } 46 | 47 | setUniform(name, value) { 48 | const uniform = this.uniforms[name]; 49 | uniform.set(value); 50 | } 51 | 52 | setAttribute(name, value) { 53 | const attribute = this.attributes[name]; 54 | attribute.set(value); 55 | } 56 | 57 | _initializeAttributes() { 58 | const attrCount = this._getParam(this.gl.ACTIVE_ATTRIBUTES); 59 | 60 | for (let i = 0; i < attrCount; i++) { 61 | const info = this.gl.getActiveAttrib(this[PROGRAM], i); 62 | this.attributes[info.name] = new Attribute(this.gl, this[PROGRAM], info); 63 | } 64 | 65 | // console.log(`Initialized attributes: ${Object.keys(this.attributes)}`); 66 | } 67 | 68 | _initializeUniforms() { 69 | const uniformCount = this._getParam(this.gl.ACTIVE_UNIFORMS); 70 | 71 | for (let i = 0; i < uniformCount; i++) { 72 | const info = this.gl.getActiveUniform(this[PROGRAM], i); 73 | this.uniforms[info.name] = new Uniform(this.gl, this[PROGRAM], info); 74 | } 75 | 76 | // console.log(`Initialized uniforms: ${Object.keys(this.uniforms)}`); 77 | } 78 | 79 | _createShader(src, glType) { 80 | const shader = this.gl.createShader(glType); 81 | this.gl.shaderSource(shader, src); 82 | 83 | // TODO compile lazily 84 | this.gl.compileShader(shader); 85 | 86 | if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { 87 | console.error(this.gl.getShaderInfoLog(shader)); 88 | } 89 | 90 | return shader; 91 | } 92 | 93 | _attachAndLink() { 94 | this.gl.attachShader(this[PROGRAM], this[VS]); 95 | this.gl.attachShader(this[PROGRAM], this[FS]); 96 | this.gl.linkProgram(this[PROGRAM]); 97 | if (!this._getParam(this.gl.LINK_STATUS)) { 98 | const error = this.gl.getProgramInfoLog(this[PROGRAM]); 99 | console.error(`Error linking program: ${error}`); 100 | } 101 | } 102 | 103 | /** 104 | * @param {GLenum} 105 | */ 106 | _getParam(parameter) { 107 | assert(parameter, 'must provide a parameter'); 108 | return this.gl.getProgramParameter(this[PROGRAM], parameter); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/webgl/programs.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Program from './program'; 3 | 4 | const PROGRAMS = Symbol('programs'); 5 | 6 | export default class Programs { 7 | /** 8 | * @param {WebGLRenderingContext} gl 9 | */ 10 | constructor(gl) { 11 | this.gl = gl; 12 | this[PROGRAMS] = new Map(); 13 | } 14 | 15 | /** 16 | * @param {Material} material 17 | * @return {Program} 18 | */ 19 | getProgram(material) { 20 | 21 | // Get the program from the cache if we've seen this 22 | // material before 23 | if (this[PROGRAMS].has(material)) { 24 | return this[PROGRAMS].get(material); 25 | } 26 | 27 | const vertSrc = material.getVertexSource(); 28 | const fragSrc = material.getFragmentSource(); 29 | 30 | // Otherwise, check to see if the frag and vert shaders 31 | // are the same as another program already in the cache; 32 | // this can occur when we use default materials, 33 | // creating new instances so each instance has its own 34 | // uniforms, but still should use the same underlying program/shaders. 35 | for (let program of this[PROGRAMS].values()) { 36 | if (program.getVertexSource() === vertSrc && 37 | program.getFragmentSource() === fragSrc) { 38 | this[PROGRAMS].set(material, program); 39 | return program; 40 | } 41 | } 42 | 43 | const program = new Program(this.gl, vertSrc, fragSrc); 44 | this[PROGRAMS].set(material, program); 45 | return program; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/webgl/register.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | const TYPES = { 3 | 0x8B50: 'FLOAT_VEC2', 4 | 0x8B51: 'FLOAT_VEC3', 5 | 0x8B52: 'FLOAT_VEC4', 6 | 0x8B53: 'INT_VEC2', 7 | 0x8B54: 'INT_VEC3', 8 | 0x8B55: 'INT_VEC4', 9 | 0x8B56: 'BOOL', 10 | 0x8B57: 'BOOL_VEC2', 11 | 0x8B58: 'BOOL_VEC3', 12 | 0x8B59: 'BOOL_VEC4', 13 | 0x8B5A: 'FLOAT_MAT2', 14 | 0x8B5B: 'FLOAT_MAT3', 15 | 0x8B5C: 'FLOAT_MAT4', 16 | 0x8B5E: 'SAMPLER_2D', 17 | 0x8B60: 'SAMPLER_CUBE', 18 | 0x1400: 'BYTE', 19 | 0x1401: 'UNSIGNED_BYTE', 20 | 0x1402: 'SHORT', 21 | 0x1403: 'UNSIGNED_SHORT', 22 | 0x1404: 'INT', 23 | 0x1405: 'UNSIGNED_INT', 24 | 0x1406: 'FLOAT' 25 | }; 26 | 27 | export default class Register { 28 | constructor(gl, program, info) { 29 | assert(typeof info.name === 'string', 'name must be a string'); 30 | assert(typeof info.size === 'number', 'size must be a number'); 31 | assert(typeof info.type === 'number', 'type must be a number'); 32 | this.gl = gl; 33 | this.program = program; 34 | this.name = info.name; 35 | this.size = info.size; 36 | this.type = info.type; 37 | this.value = null; 38 | this.loc = this._getLocation(); 39 | } 40 | 41 | _getLocation() { 42 | throw new Error('must be implemented by inheriting class'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/webgl/uniform.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Register from './register'; 3 | 4 | export default class Uniform extends Register { 5 | constructor(gl, program, info) { 6 | super(gl, program, info); 7 | } 8 | 9 | _getLocation() { 10 | return this.gl.getUniformLocation(this.program, this.name); 11 | } 12 | 13 | set(value) { 14 | const { gl, loc, type } = this; 15 | 16 | // Check if this is a Math type of ours to extract 17 | // the typed array data 18 | if (value && (value.isVector || value.isMatrix)) { 19 | // Continue setting values with the underlying 20 | // TypedArray 21 | value = value.getArray(); 22 | } 23 | 24 | switch (type) { 25 | case gl.INT: 26 | gl.uniform1i(loc, value); 27 | break; 28 | case gl.INT_VEC2: 29 | gl.uniform2iv(loc, value); 30 | break; 31 | case gl.INT_VEC3: 32 | gl.uniform3iv(loc, value); 33 | break; 34 | case gl.INT_VEC4: 35 | gl.uniform4iv(loc, value); 36 | break; 37 | case gl.FLOAT: 38 | gl.uniform1f(loc, value); 39 | break; 40 | case gl.FLOAT_VEC2: 41 | gl.uniform2fv(loc, value); 42 | break; 43 | case gl.FLOAT_VEC3: 44 | gl.uniform3fv(loc, value); 45 | break; 46 | case gl.FLOAT_VEC4: 47 | gl.uniform4fv(loc, value); 48 | break; 49 | case gl.FLOAT_MAT3: 50 | gl.uniformMatrix3fv(loc, false, value); 51 | break; 52 | case gl.FLOAT_MAT4: 53 | gl.uniformMatrix4fv(loc, false, value); 54 | break; 55 | default: 56 | throw new Error('Unexpected uniform type'); 57 | } 58 | 59 | this.value = value; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vendor/webgl-debug.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2012 The Khronos Group Inc. 3 | ** 4 | ** Permission is hereby granted, free of charge, to any person obtaining a 5 | ** copy of this software and/or associated documentation files (the 6 | ** "Materials"), to deal in the Materials without restriction, including 7 | ** without limitation the rights to use, copy, modify, merge, publish, 8 | ** distribute, sublicense, and/or sell copies of the Materials, and to 9 | ** permit persons to whom the Materials are furnished to do so, subject to 10 | ** the following conditions: 11 | ** 12 | ** The above copyright notice and this permission notice shall be included 13 | ** in all copies or substantial portions of the Materials. 14 | ** 15 | ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. 22 | */ 23 | 24 | // Various functions for helping debug WebGL apps. 25 | 26 | WebGLDebugUtils = function() { 27 | 28 | /** 29 | * Wrapped logging function. 30 | * @param {string} msg Message to log. 31 | */ 32 | var log = function(msg) { 33 | if (window.console && window.console.log) { 34 | window.console.log(msg); 35 | } 36 | }; 37 | 38 | /** 39 | * Wrapped error logging function. 40 | * @param {string} msg Message to log. 41 | */ 42 | var error = function(msg) { 43 | if (window.console && window.console.error) { 44 | window.console.error(msg); 45 | } else { 46 | log(msg); 47 | } 48 | }; 49 | 50 | 51 | /** 52 | * Which arguments are enums based on the number of arguments to the function. 53 | * So 54 | * 'texImage2D': { 55 | * 9: { 0:true, 2:true, 6:true, 7:true }, 56 | * 6: { 0:true, 2:true, 3:true, 4:true }, 57 | * }, 58 | * 59 | * means if there are 9 arguments then 6 and 7 are enums, if there are 6 60 | * arguments 3 and 4 are enums 61 | * 62 | * @type {!Object.} 63 | */ 64 | var glValidEnumContexts = { 65 | // Generic setters and getters 66 | 67 | 'enable': {1: { 0:true }}, 68 | 'disable': {1: { 0:true }}, 69 | 'getParameter': {1: { 0:true }}, 70 | 71 | // Rendering 72 | 73 | 'drawArrays': {3:{ 0:true }}, 74 | 'drawElements': {4:{ 0:true, 2:true }}, 75 | 76 | // Shaders 77 | 78 | 'createShader': {1: { 0:true }}, 79 | 'getShaderParameter': {2: { 1:true }}, 80 | 'getProgramParameter': {2: { 1:true }}, 81 | 'getShaderPrecisionFormat': {2: { 0: true, 1:true }}, 82 | 83 | // Vertex attributes 84 | 85 | 'getVertexAttrib': {2: { 1:true }}, 86 | 'vertexAttribPointer': {6: { 2:true }}, 87 | 88 | // Textures 89 | 90 | 'bindTexture': {2: { 0:true }}, 91 | 'activeTexture': {1: { 0:true }}, 92 | 'getTexParameter': {2: { 0:true, 1:true }}, 93 | 'texParameterf': {3: { 0:true, 1:true }}, 94 | 'texParameteri': {3: { 0:true, 1:true, 2:true }}, 95 | // texImage2D and texSubImage2D are defined below with WebGL 2 entrypoints 96 | 'copyTexImage2D': {8: { 0:true, 2:true }}, 97 | 'copyTexSubImage2D': {8: { 0:true }}, 98 | 'generateMipmap': {1: { 0:true }}, 99 | // compressedTexImage2D and compressedTexSubImage2D are defined below with WebGL 2 entrypoints 100 | 101 | // Buffer objects 102 | 103 | 'bindBuffer': {2: { 0:true }}, 104 | // bufferData and bufferSubData are defined below with WebGL 2 entrypoints 105 | 'getBufferParameter': {2: { 0:true, 1:true }}, 106 | 107 | // Renderbuffers and framebuffers 108 | 109 | 'pixelStorei': {2: { 0:true, 1:true }}, 110 | // readPixels is defined below with WebGL 2 entrypoints 111 | 'bindRenderbuffer': {2: { 0:true }}, 112 | 'bindFramebuffer': {2: { 0:true }}, 113 | 'checkFramebufferStatus': {1: { 0:true }}, 114 | 'framebufferRenderbuffer': {4: { 0:true, 1:true, 2:true }}, 115 | 'framebufferTexture2D': {5: { 0:true, 1:true, 2:true }}, 116 | 'getFramebufferAttachmentParameter': {3: { 0:true, 1:true, 2:true }}, 117 | 'getRenderbufferParameter': {2: { 0:true, 1:true }}, 118 | 'renderbufferStorage': {4: { 0:true, 1:true }}, 119 | 120 | // Frame buffer operations (clear, blend, depth test, stencil) 121 | 122 | 'clear': {1: { 0: { 'enumBitwiseOr': ['COLOR_BUFFER_BIT', 'DEPTH_BUFFER_BIT', 'STENCIL_BUFFER_BIT'] }}}, 123 | 'depthFunc': {1: { 0:true }}, 124 | 'blendFunc': {2: { 0:true, 1:true }}, 125 | 'blendFuncSeparate': {4: { 0:true, 1:true, 2:true, 3:true }}, 126 | 'blendEquation': {1: { 0:true }}, 127 | 'blendEquationSeparate': {2: { 0:true, 1:true }}, 128 | 'stencilFunc': {3: { 0:true }}, 129 | 'stencilFuncSeparate': {4: { 0:true, 1:true }}, 130 | 'stencilMaskSeparate': {2: { 0:true }}, 131 | 'stencilOp': {3: { 0:true, 1:true, 2:true }}, 132 | 'stencilOpSeparate': {4: { 0:true, 1:true, 2:true, 3:true }}, 133 | 134 | // Culling 135 | 136 | 'cullFace': {1: { 0:true }}, 137 | 'frontFace': {1: { 0:true }}, 138 | 139 | // ANGLE_instanced_arrays extension 140 | 141 | 'drawArraysInstancedANGLE': {4: { 0:true }}, 142 | 'drawElementsInstancedANGLE': {5: { 0:true, 2:true }}, 143 | 144 | // EXT_blend_minmax extension 145 | 146 | 'blendEquationEXT': {1: { 0:true }}, 147 | 148 | // WebGL 2 Buffer objects 149 | 150 | 'bufferData': { 151 | 3: { 0:true, 2:true }, // WebGL 1 152 | 4: { 0:true, 2:true }, // WebGL 2 153 | 5: { 0:true, 2:true } // WebGL 2 154 | }, 155 | 'bufferSubData': { 156 | 3: { 0:true }, // WebGL 1 157 | 4: { 0:true }, // WebGL 2 158 | 5: { 0:true } // WebGL 2 159 | }, 160 | 'copyBufferSubData': {5: { 0:true, 1:true }}, 161 | 'getBufferSubData': {3: { 0:true }, 4: { 0:true }, 5: { 0:true }}, 162 | 163 | // WebGL 2 Framebuffer objects 164 | 165 | 'blitFramebuffer': {10: { 8: { 'enumBitwiseOr': ['COLOR_BUFFER_BIT', 'DEPTH_BUFFER_BIT', 'STENCIL_BUFFER_BIT'] }, 9:true }}, 166 | 'framebufferTextureLayer': {5: { 0:true, 1:true }}, 167 | 'invalidateFramebuffer': {2: { 0:true }}, 168 | 'invalidateSubFramebuffer': {6: { 0:true }}, 169 | 'readBuffer': {1: { 0:true }}, 170 | 171 | // WebGL 2 Renderbuffer objects 172 | 173 | 'getInternalformatParameter': {3: { 0:true, 1:true, 2:true }}, 174 | 'renderbufferStorageMultisample': {5: { 0:true, 2:true }}, 175 | 176 | // WebGL 2 Texture objects 177 | 178 | 'texStorage2D': {5: { 0:true, 2:true }}, 179 | 'texStorage3D': {6: { 0:true, 2:true }}, 180 | 'texImage2D': { 181 | 9: { 0:true, 2:true, 6:true, 7:true }, // WebGL 1 & 2 182 | 6: { 0:true, 2:true, 3:true, 4:true }, // WebGL 1 183 | 10: { 0:true, 2:true, 6:true, 7:true } // WebGL 2 184 | }, 185 | 'texImage3D': { 186 | 10: { 0:true, 2:true, 7:true, 8:true }, 187 | 11: { 0:true, 2:true, 7:true, 8:true } 188 | }, 189 | 'texSubImage2D': { 190 | 9: { 0:true, 6:true, 7:true }, // WebGL 1 & 2 191 | 7: { 0:true, 4:true, 5:true }, // WebGL 1 192 | 10: { 0:true, 6:true, 7:true } // WebGL 2 193 | }, 194 | 'texSubImage3D': { 195 | 11: { 0:true, 8:true, 9:true }, 196 | 12: { 0:true, 8:true, 9:true } 197 | }, 198 | 'copyTexSubImage3D': {9: { 0:true }}, 199 | 'compressedTexImage2D': { 200 | 7: { 0: true, 2:true }, // WebGL 1 & 2 201 | 8: { 0: true, 2:true }, // WebGL 2 202 | 9: { 0: true, 2:true } // WebGL 2 203 | }, 204 | 'compressedTexImage3D': { 205 | 8: { 0: true, 2:true }, 206 | 9: { 0: true, 2:true }, 207 | 10: { 0: true, 2:true } 208 | }, 209 | 'compressedTexSubImage2D': { 210 | 8: { 0: true, 6:true }, // WebGL 1 & 2 211 | 9: { 0: true, 6:true }, // WebGL 2 212 | 10: { 0: true, 6:true } // WebGL 2 213 | }, 214 | 'compressedTexSubImage3D': { 215 | 10: { 0: true, 8:true }, 216 | 11: { 0: true, 8:true }, 217 | 12: { 0: true, 8:true } 218 | }, 219 | 220 | // WebGL 2 Vertex attribs 221 | 222 | 'vertexAttribIPointer': {5: { 2:true }}, 223 | 224 | // WebGL 2 Writing to the drawing buffer 225 | 226 | 'drawArraysInstanced': {4: { 0:true }}, 227 | 'drawElementsInstanced': {5: { 0:true, 2:true }}, 228 | 'drawRangeElements': {6: { 0:true, 4:true }}, 229 | 230 | // WebGL 2 Reading back pixels 231 | 232 | 'readPixels': { 233 | 7: { 4:true, 5:true }, // WebGL 1 & 2 234 | 8: { 4:true, 5:true } // WebGL 2 235 | }, 236 | 237 | // WebGL 2 Multiple Render Targets 238 | 239 | 'clearBufferfv': {3: { 0:true }, 4: { 0:true }}, 240 | 'clearBufferiv': {3: { 0:true }, 4: { 0:true }}, 241 | 'clearBufferuiv': {3: { 0:true }, 4: { 0:true }}, 242 | 'clearBufferfi': {4: { 0:true }}, 243 | 244 | // WebGL 2 Query objects 245 | 246 | 'beginQuery': {2: { 0:true }}, 247 | 'endQuery': {1: { 0:true }}, 248 | 'getQuery': {2: { 0:true, 1:true }}, 249 | 'getQueryParameter': {2: { 1:true }}, 250 | 251 | // WebGL 2 Sampler objects 252 | 253 | 'samplerParameteri': {3: { 1:true, 2:true }}, 254 | 'samplerParameterf': {3: { 1:true }}, 255 | 'getSamplerParameter': {2: { 1:true }}, 256 | 257 | // WebGL 2 Sync objects 258 | 259 | 'fenceSync': {2: { 0:true, 1: { 'enumBitwiseOr': [] } }}, 260 | 'clientWaitSync': {3: { 1: { 'enumBitwiseOr': ['SYNC_FLUSH_COMMANDS_BIT'] } }}, 261 | 'waitSync': {3: { 1: { 'enumBitwiseOr': [] } }}, 262 | 'getSyncParameter': {2: { 1:true }}, 263 | 264 | // WebGL 2 Transform Feedback 265 | 266 | 'bindTransformFeedback': {2: { 0:true }}, 267 | 'beginTransformFeedback': {1: { 0:true }}, 268 | 'transformFeedbackVaryings': {3: { 2:true }}, 269 | 270 | // WebGL2 Uniform Buffer Objects and Transform Feedback Buffers 271 | 272 | 'bindBufferBase': {3: { 0:true }}, 273 | 'bindBufferRange': {5: { 0:true }}, 274 | 'getIndexedParameter': {2: { 0:true }}, 275 | 'getActiveUniforms': {3: { 2:true }}, 276 | 'getActiveUniformBlockParameter': {3: { 2:true }} 277 | }; 278 | 279 | /** 280 | * Map of numbers to names. 281 | * @type {Object} 282 | */ 283 | var glEnums = null; 284 | 285 | /** 286 | * Map of names to numbers. 287 | * @type {Object} 288 | */ 289 | var enumStringToValue = null; 290 | 291 | /** 292 | * Initializes this module. Safe to call more than once. 293 | * @param {!WebGLRenderingContext} ctx A WebGL context. If 294 | * you have more than one context it doesn't matter which one 295 | * you pass in, it is only used to pull out constants. 296 | */ 297 | function init(ctx) { 298 | if (glEnums == null) { 299 | glEnums = { }; 300 | enumStringToValue = { }; 301 | for (var propertyName in ctx) { 302 | if (typeof ctx[propertyName] == 'number') { 303 | glEnums[ctx[propertyName]] = propertyName; 304 | enumStringToValue[propertyName] = ctx[propertyName]; 305 | } 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * Checks the utils have been initialized. 312 | */ 313 | function checkInit() { 314 | if (glEnums == null) { 315 | throw 'WebGLDebugUtils.init(ctx) not called'; 316 | } 317 | } 318 | 319 | /** 320 | * Returns true or false if value matches any WebGL enum 321 | * @param {*} value Value to check if it might be an enum. 322 | * @return {boolean} True if value matches one of the WebGL defined enums 323 | */ 324 | function mightBeEnum(value) { 325 | checkInit(); 326 | return (glEnums[value] !== undefined); 327 | } 328 | 329 | /** 330 | * Gets an string version of an WebGL enum. 331 | * 332 | * Example: 333 | * var str = WebGLDebugUtil.glEnumToString(ctx.getError()); 334 | * 335 | * @param {number} value Value to return an enum for 336 | * @return {string} The string version of the enum. 337 | */ 338 | function glEnumToString(value) { 339 | checkInit(); 340 | var name = glEnums[value]; 341 | return (name !== undefined) ? ("gl." + name) : 342 | ("/*UNKNOWN WebGL ENUM*/ 0x" + value.toString(16) + ""); 343 | } 344 | 345 | /** 346 | * Returns the string version of a WebGL argument. 347 | * Attempts to convert enum arguments to strings. 348 | * @param {string} functionName the name of the WebGL function. 349 | * @param {number} numArgs the number of arguments passed to the function. 350 | * @param {number} argumentIndx the index of the argument. 351 | * @param {*} value The value of the argument. 352 | * @return {string} The value as a string. 353 | */ 354 | function glFunctionArgToString(functionName, numArgs, argumentIndex, value) { 355 | var funcInfo = glValidEnumContexts[functionName]; 356 | if (funcInfo !== undefined) { 357 | var funcInfo = funcInfo[numArgs]; 358 | if (funcInfo !== undefined) { 359 | if (funcInfo[argumentIndex]) { 360 | if (typeof funcInfo[argumentIndex] === 'object' && 361 | funcInfo[argumentIndex]['enumBitwiseOr'] !== undefined) { 362 | var enums = funcInfo[argumentIndex]['enumBitwiseOr']; 363 | var orResult = 0; 364 | var orEnums = []; 365 | for (var i = 0; i < enums.length; ++i) { 366 | var enumValue = enumStringToValue[enums[i]]; 367 | if ((value & enumValue) !== 0) { 368 | orResult |= enumValue; 369 | orEnums.push(glEnumToString(enumValue)); 370 | } 371 | } 372 | if (orResult === value) { 373 | return orEnums.join(' | '); 374 | } else { 375 | return glEnumToString(value); 376 | } 377 | } else { 378 | return glEnumToString(value); 379 | } 380 | } 381 | } 382 | } 383 | if (value === null) { 384 | return "null"; 385 | } else if (value === undefined) { 386 | return "undefined"; 387 | } else { 388 | return value.toString(); 389 | } 390 | } 391 | 392 | /** 393 | * Converts the arguments of a WebGL function to a string. 394 | * Attempts to convert enum arguments to strings. 395 | * 396 | * @param {string} functionName the name of the WebGL function. 397 | * @param {number} args The arguments. 398 | * @return {string} The arguments as a string. 399 | */ 400 | function glFunctionArgsToString(functionName, args) { 401 | // apparently we can't do args.join(","); 402 | var argStr = ""; 403 | var numArgs = args.length; 404 | for (var ii = 0; ii < numArgs; ++ii) { 405 | argStr += ((ii == 0) ? '' : ', ') + 406 | glFunctionArgToString(functionName, numArgs, ii, args[ii]); 407 | } 408 | return argStr; 409 | }; 410 | 411 | 412 | function makePropertyWrapper(wrapper, original, propertyName) { 413 | //log("wrap prop: " + propertyName); 414 | wrapper.__defineGetter__(propertyName, function() { 415 | return original[propertyName]; 416 | }); 417 | // TODO(gmane): this needs to handle properties that take more than 418 | // one value? 419 | wrapper.__defineSetter__(propertyName, function(value) { 420 | //log("set: " + propertyName); 421 | original[propertyName] = value; 422 | }); 423 | } 424 | 425 | // Makes a function that calls a function on another object. 426 | function makeFunctionWrapper(original, functionName) { 427 | //log("wrap fn: " + functionName); 428 | var f = original[functionName]; 429 | return function() { 430 | //log("call: " + functionName); 431 | var result = f.apply(original, arguments); 432 | return result; 433 | }; 434 | } 435 | 436 | /** 437 | * Given a WebGL context returns a wrapped context that calls 438 | * gl.getError after every command and calls a function if the 439 | * result is not gl.NO_ERROR. 440 | * 441 | * @param {!WebGLRenderingContext} ctx The webgl context to 442 | * wrap. 443 | * @param {!function(err, funcName, args): void} opt_onErrorFunc 444 | * The function to call when gl.getError returns an 445 | * error. If not specified the default function calls 446 | * console.log with a message. 447 | * @param {!function(funcName, args): void} opt_onFunc The 448 | * function to call when each webgl function is called. 449 | * You can use this to log all calls for example. 450 | * @param {!WebGLRenderingContext} opt_err_ctx The webgl context 451 | * to call getError on if different than ctx. 452 | */ 453 | function makeDebugContext(ctx, opt_onErrorFunc, opt_onFunc, opt_err_ctx) { 454 | opt_err_ctx = opt_err_ctx || ctx; 455 | init(ctx); 456 | opt_onErrorFunc = opt_onErrorFunc || function(err, functionName, args) { 457 | // apparently we can't do args.join(","); 458 | var argStr = ""; 459 | var numArgs = args.length; 460 | for (var ii = 0; ii < numArgs; ++ii) { 461 | argStr += ((ii == 0) ? '' : ', ') + 462 | glFunctionArgToString(functionName, numArgs, ii, args[ii]); 463 | } 464 | error("WebGL error "+ glEnumToString(err) + " in "+ functionName + 465 | "(" + argStr + ")"); 466 | }; 467 | 468 | // Holds booleans for each GL error so after we get the error ourselves 469 | // we can still return it to the client app. 470 | var glErrorShadow = { }; 471 | 472 | // Makes a function that calls a WebGL function and then calls getError. 473 | function makeErrorWrapper(ctx, functionName) { 474 | return function() { 475 | if (opt_onFunc) { 476 | opt_onFunc(functionName, arguments); 477 | } 478 | var result = ctx[functionName].apply(ctx, arguments); 479 | var err = opt_err_ctx.getError(); 480 | if (err != 0) { 481 | glErrorShadow[err] = true; 482 | opt_onErrorFunc(err, functionName, arguments); 483 | } 484 | return result; 485 | }; 486 | } 487 | 488 | // Make a an object that has a copy of every property of the WebGL context 489 | // but wraps all functions. 490 | var wrapper = {}; 491 | for (var propertyName in ctx) { 492 | if (typeof ctx[propertyName] == 'function') { 493 | if (propertyName != 'getExtension') { 494 | wrapper[propertyName] = makeErrorWrapper(ctx, propertyName); 495 | } else { 496 | var wrapped = makeErrorWrapper(ctx, propertyName); 497 | wrapper[propertyName] = function () { 498 | var result = wrapped.apply(ctx, arguments); 499 | if (!result) { 500 | return null; 501 | } 502 | return makeDebugContext(result, opt_onErrorFunc, opt_onFunc, opt_err_ctx); 503 | }; 504 | } 505 | } else { 506 | makePropertyWrapper(wrapper, ctx, propertyName); 507 | } 508 | } 509 | 510 | // Override the getError function with one that returns our saved results. 511 | wrapper.getError = function() { 512 | for (var err in glErrorShadow) { 513 | if (glErrorShadow.hasOwnProperty(err)) { 514 | if (glErrorShadow[err]) { 515 | glErrorShadow[err] = false; 516 | return err; 517 | } 518 | } 519 | } 520 | return ctx.NO_ERROR; 521 | }; 522 | 523 | return wrapper; 524 | } 525 | 526 | function resetToInitialState(ctx) { 527 | var isWebGL2RenderingContext = !!ctx.createTransformFeedback; 528 | 529 | if (isWebGL2RenderingContext) { 530 | ctx.bindVertexArray(null); 531 | } 532 | 533 | var numAttribs = ctx.getParameter(ctx.MAX_VERTEX_ATTRIBS); 534 | var tmp = ctx.createBuffer(); 535 | ctx.bindBuffer(ctx.ARRAY_BUFFER, tmp); 536 | for (var ii = 0; ii < numAttribs; ++ii) { 537 | ctx.disableVertexAttribArray(ii); 538 | ctx.vertexAttribPointer(ii, 4, ctx.FLOAT, false, 0, 0); 539 | ctx.vertexAttrib1f(ii, 0); 540 | if (isWebGL2RenderingContext) { 541 | ctx.vertexAttribDivisor(ii, 0); 542 | } 543 | } 544 | ctx.deleteBuffer(tmp); 545 | 546 | var numTextureUnits = ctx.getParameter(ctx.MAX_TEXTURE_IMAGE_UNITS); 547 | for (var ii = 0; ii < numTextureUnits; ++ii) { 548 | ctx.activeTexture(ctx.TEXTURE0 + ii); 549 | ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, null); 550 | ctx.bindTexture(ctx.TEXTURE_2D, null); 551 | if (isWebGL2RenderingContext) { 552 | ctx.bindTexture(ctx.TEXTURE_2D_ARRAY, null); 553 | ctx.bindTexture(ctx.TEXTURE_3D, null); 554 | ctx.bindSampler(ii, null); 555 | } 556 | } 557 | 558 | ctx.activeTexture(ctx.TEXTURE0); 559 | ctx.useProgram(null); 560 | ctx.bindBuffer(ctx.ARRAY_BUFFER, null); 561 | ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null); 562 | ctx.bindFramebuffer(ctx.FRAMEBUFFER, null); 563 | ctx.bindRenderbuffer(ctx.RENDERBUFFER, null); 564 | ctx.disable(ctx.BLEND); 565 | ctx.disable(ctx.CULL_FACE); 566 | ctx.disable(ctx.DEPTH_TEST); 567 | ctx.disable(ctx.DITHER); 568 | ctx.disable(ctx.SCISSOR_TEST); 569 | ctx.blendColor(0, 0, 0, 0); 570 | ctx.blendEquation(ctx.FUNC_ADD); 571 | ctx.blendFunc(ctx.ONE, ctx.ZERO); 572 | ctx.clearColor(0, 0, 0, 0); 573 | ctx.clearDepth(1); 574 | ctx.clearStencil(-1); 575 | ctx.colorMask(true, true, true, true); 576 | ctx.cullFace(ctx.BACK); 577 | ctx.depthFunc(ctx.LESS); 578 | ctx.depthMask(true); 579 | ctx.depthRange(0, 1); 580 | ctx.frontFace(ctx.CCW); 581 | ctx.hint(ctx.GENERATE_MIPMAP_HINT, ctx.DONT_CARE); 582 | ctx.lineWidth(1); 583 | ctx.pixelStorei(ctx.PACK_ALIGNMENT, 4); 584 | ctx.pixelStorei(ctx.UNPACK_ALIGNMENT, 4); 585 | ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false); 586 | ctx.pixelStorei(ctx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); 587 | // TODO: Delete this IF. 588 | if (ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL) { 589 | ctx.pixelStorei(ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL, ctx.BROWSER_DEFAULT_WEBGL); 590 | } 591 | ctx.polygonOffset(0, 0); 592 | ctx.sampleCoverage(1, false); 593 | ctx.scissor(0, 0, ctx.canvas.width, ctx.canvas.height); 594 | ctx.stencilFunc(ctx.ALWAYS, 0, 0xFFFFFFFF); 595 | ctx.stencilMask(0xFFFFFFFF); 596 | ctx.stencilOp(ctx.KEEP, ctx.KEEP, ctx.KEEP); 597 | ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height); 598 | ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT); 599 | 600 | if (isWebGL2RenderingContext) { 601 | ctx.drawBuffers([ctx.BACK]); 602 | ctx.readBuffer(ctx.BACK); 603 | ctx.bindBuffer(ctx.COPY_READ_BUFFER, null); 604 | ctx.bindBuffer(ctx.COPY_WRITE_BUFFER, null); 605 | ctx.bindBuffer(ctx.PIXEL_PACK_BUFFER, null); 606 | ctx.bindBuffer(ctx.PIXEL_UNPACK_BUFFER, null); 607 | var numTransformFeedbacks = ctx.getParameter(ctx.MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS); 608 | for (var ii = 0; ii < numTransformFeedbacks; ++ii) { 609 | ctx.bindBufferBase(ctx.TRANSFORM_FEEDBACK_BUFFER, ii, null); 610 | } 611 | var numUBOs = ctx.getParameter(ctx.MAX_UNIFORM_BUFFER_BINDINGS); 612 | for (var ii = 0; ii < numUBOs; ++ii) { 613 | ctx.bindBufferBase(ctx.UNIFORM_BUFFER, ii, null); 614 | } 615 | ctx.disable(ctx.RASTERIZER_DISCARD); 616 | ctx.pixelStorei(ctx.UNPACK_IMAGE_HEIGHT, 0); 617 | ctx.pixelStorei(ctx.UNPACK_SKIP_IMAGES, 0); 618 | ctx.pixelStorei(ctx.UNPACK_ROW_LENGTH, 0); 619 | ctx.pixelStorei(ctx.UNPACK_SKIP_ROWS, 0); 620 | ctx.pixelStorei(ctx.UNPACK_SKIP_PIXELS, 0); 621 | ctx.pixelStorei(ctx.PACK_ROW_LENGTH, 0); 622 | ctx.pixelStorei(ctx.PACK_SKIP_ROWS, 0); 623 | ctx.pixelStorei(ctx.PACK_SKIP_PIXELS, 0); 624 | ctx.hint(ctx.FRAGMENT_SHADER_DERIVATIVE_HINT, ctx.DONT_CARE); 625 | } 626 | 627 | // TODO: This should NOT be needed but Firefox fails with 'hint' 628 | while(ctx.getError()); 629 | } 630 | 631 | function makeLostContextSimulatingCanvas(canvas) { 632 | var unwrappedContext_; 633 | var wrappedContext_; 634 | var onLost_ = []; 635 | var onRestored_ = []; 636 | var wrappedContext_ = {}; 637 | var contextId_ = 1; 638 | var contextLost_ = false; 639 | var resourceId_ = 0; 640 | var resourceDb_ = []; 641 | var numCallsToLoseContext_ = 0; 642 | var numCalls_ = 0; 643 | var canRestore_ = false; 644 | var restoreTimeout_ = 0; 645 | var isWebGL2RenderingContext; 646 | 647 | // Holds booleans for each GL error so can simulate errors. 648 | var glErrorShadow_ = { }; 649 | 650 | canvas.getContext = function(f) { 651 | return function() { 652 | var ctx = f.apply(canvas, arguments); 653 | // Did we get a context and is it a WebGL context? 654 | if ((ctx instanceof WebGLRenderingContext) || (window.WebGL2RenderingContext && (ctx instanceof WebGL2RenderingContext))) { 655 | if (ctx != unwrappedContext_) { 656 | if (unwrappedContext_) { 657 | throw "got different context" 658 | } 659 | isWebGL2RenderingContext = window.WebGL2RenderingContext && (ctx instanceof WebGL2RenderingContext); 660 | unwrappedContext_ = ctx; 661 | wrappedContext_ = makeLostContextSimulatingContext(unwrappedContext_); 662 | } 663 | return wrappedContext_; 664 | } 665 | return ctx; 666 | } 667 | }(canvas.getContext); 668 | 669 | function wrapEvent(listener) { 670 | if (typeof(listener) == "function") { 671 | return listener; 672 | } else { 673 | return function(info) { 674 | listener.handleEvent(info); 675 | } 676 | } 677 | } 678 | 679 | var addOnContextLostListener = function(listener) { 680 | onLost_.push(wrapEvent(listener)); 681 | }; 682 | 683 | var addOnContextRestoredListener = function(listener) { 684 | onRestored_.push(wrapEvent(listener)); 685 | }; 686 | 687 | 688 | function wrapAddEventListener(canvas) { 689 | var f = canvas.addEventListener; 690 | canvas.addEventListener = function(type, listener, bubble) { 691 | switch (type) { 692 | case 'webglcontextlost': 693 | addOnContextLostListener(listener); 694 | break; 695 | case 'webglcontextrestored': 696 | addOnContextRestoredListener(listener); 697 | break; 698 | default: 699 | f.apply(canvas, arguments); 700 | } 701 | }; 702 | } 703 | 704 | wrapAddEventListener(canvas); 705 | 706 | canvas.loseContext = function() { 707 | if (!contextLost_) { 708 | contextLost_ = true; 709 | numCallsToLoseContext_ = 0; 710 | ++contextId_; 711 | while (unwrappedContext_.getError()); 712 | clearErrors(); 713 | glErrorShadow_[unwrappedContext_.CONTEXT_LOST_WEBGL] = true; 714 | var event = makeWebGLContextEvent("context lost"); 715 | var callbacks = onLost_.slice(); 716 | setTimeout(function() { 717 | //log("numCallbacks:" + callbacks.length); 718 | for (var ii = 0; ii < callbacks.length; ++ii) { 719 | //log("calling callback:" + ii); 720 | callbacks[ii](event); 721 | } 722 | if (restoreTimeout_ >= 0) { 723 | setTimeout(function() { 724 | canvas.restoreContext(); 725 | }, restoreTimeout_); 726 | } 727 | }, 0); 728 | } 729 | }; 730 | 731 | canvas.restoreContext = function() { 732 | if (contextLost_) { 733 | if (onRestored_.length) { 734 | setTimeout(function() { 735 | if (!canRestore_) { 736 | throw "can not restore. webglcontestlost listener did not call event.preventDefault"; 737 | } 738 | freeResources(); 739 | resetToInitialState(unwrappedContext_); 740 | contextLost_ = false; 741 | numCalls_ = 0; 742 | canRestore_ = false; 743 | var callbacks = onRestored_.slice(); 744 | var event = makeWebGLContextEvent("context restored"); 745 | for (var ii = 0; ii < callbacks.length; ++ii) { 746 | callbacks[ii](event); 747 | } 748 | }, 0); 749 | } 750 | } 751 | }; 752 | 753 | canvas.loseContextInNCalls = function(numCalls) { 754 | if (contextLost_) { 755 | throw "You can not ask a lost contet to be lost"; 756 | } 757 | numCallsToLoseContext_ = numCalls_ + numCalls; 758 | }; 759 | 760 | canvas.getNumCalls = function() { 761 | return numCalls_; 762 | }; 763 | 764 | canvas.setRestoreTimeout = function(timeout) { 765 | restoreTimeout_ = timeout; 766 | }; 767 | 768 | function isWebGLObject(obj) { 769 | //return false; 770 | return (obj instanceof WebGLBuffer || 771 | obj instanceof WebGLFramebuffer || 772 | obj instanceof WebGLProgram || 773 | obj instanceof WebGLRenderbuffer || 774 | obj instanceof WebGLShader || 775 | obj instanceof WebGLTexture); 776 | } 777 | 778 | function checkResources(args) { 779 | for (var ii = 0; ii < args.length; ++ii) { 780 | var arg = args[ii]; 781 | if (isWebGLObject(arg)) { 782 | return arg.__webglDebugContextLostId__ == contextId_; 783 | } 784 | } 785 | return true; 786 | } 787 | 788 | function clearErrors() { 789 | var k = Object.keys(glErrorShadow_); 790 | for (var ii = 0; ii < k.length; ++ii) { 791 | delete glErrorShadow_[k[ii]]; 792 | } 793 | } 794 | 795 | function loseContextIfTime() { 796 | ++numCalls_; 797 | if (!contextLost_) { 798 | if (numCallsToLoseContext_ == numCalls_) { 799 | canvas.loseContext(); 800 | } 801 | } 802 | } 803 | 804 | // Makes a function that simulates WebGL when out of context. 805 | function makeLostContextFunctionWrapper(ctx, functionName) { 806 | var f = ctx[functionName]; 807 | return function() { 808 | // log("calling:" + functionName); 809 | // Only call the functions if the context is not lost. 810 | loseContextIfTime(); 811 | if (!contextLost_) { 812 | //if (!checkResources(arguments)) { 813 | // glErrorShadow_[wrappedContext_.INVALID_OPERATION] = true; 814 | // return; 815 | //} 816 | var result = f.apply(ctx, arguments); 817 | return result; 818 | } 819 | }; 820 | } 821 | 822 | function freeResources() { 823 | for (var ii = 0; ii < resourceDb_.length; ++ii) { 824 | var resource = resourceDb_[ii]; 825 | if (resource instanceof WebGLBuffer) { 826 | unwrappedContext_.deleteBuffer(resource); 827 | } else if (resource instanceof WebGLFramebuffer) { 828 | unwrappedContext_.deleteFramebuffer(resource); 829 | } else if (resource instanceof WebGLProgram) { 830 | unwrappedContext_.deleteProgram(resource); 831 | } else if (resource instanceof WebGLRenderbuffer) { 832 | unwrappedContext_.deleteRenderbuffer(resource); 833 | } else if (resource instanceof WebGLShader) { 834 | unwrappedContext_.deleteShader(resource); 835 | } else if (resource instanceof WebGLTexture) { 836 | unwrappedContext_.deleteTexture(resource); 837 | } 838 | else if (isWebGL2RenderingContext) { 839 | if (resource instanceof WebGLQuery) { 840 | unwrappedContext_.deleteQuery(resource); 841 | } else if (resource instanceof WebGLSampler) { 842 | unwrappedContext_.deleteSampler(resource); 843 | } else if (resource instanceof WebGLSync) { 844 | unwrappedContext_.deleteSync(resource); 845 | } else if (resource instanceof WebGLTransformFeedback) { 846 | unwrappedContext_.deleteTransformFeedback(resource); 847 | } else if (resource instanceof WebGLVertexArrayObject) { 848 | unwrappedContext_.deleteVertexArray(resource); 849 | } 850 | } 851 | } 852 | } 853 | 854 | function makeWebGLContextEvent(statusMessage) { 855 | return { 856 | statusMessage: statusMessage, 857 | preventDefault: function() { 858 | canRestore_ = true; 859 | } 860 | }; 861 | } 862 | 863 | return canvas; 864 | 865 | function makeLostContextSimulatingContext(ctx) { 866 | // copy all functions and properties to wrapper 867 | for (var propertyName in ctx) { 868 | if (typeof ctx[propertyName] == 'function') { 869 | wrappedContext_[propertyName] = makeLostContextFunctionWrapper( 870 | ctx, propertyName); 871 | } else { 872 | makePropertyWrapper(wrappedContext_, ctx, propertyName); 873 | } 874 | } 875 | 876 | // Wrap a few functions specially. 877 | wrappedContext_.getError = function() { 878 | loseContextIfTime(); 879 | if (!contextLost_) { 880 | var err; 881 | while (err = unwrappedContext_.getError()) { 882 | glErrorShadow_[err] = true; 883 | } 884 | } 885 | for (var err in glErrorShadow_) { 886 | if (glErrorShadow_[err]) { 887 | delete glErrorShadow_[err]; 888 | return err; 889 | } 890 | } 891 | return wrappedContext_.NO_ERROR; 892 | }; 893 | 894 | var creationFunctions = [ 895 | "createBuffer", 896 | "createFramebuffer", 897 | "createProgram", 898 | "createRenderbuffer", 899 | "createShader", 900 | "createTexture" 901 | ]; 902 | if (isWebGL2RenderingContext) { 903 | creationFunctions.push( 904 | "createQuery", 905 | "createSampler", 906 | "fenceSync", 907 | "createTransformFeedback", 908 | "createVertexArray" 909 | ); 910 | } 911 | for (var ii = 0; ii < creationFunctions.length; ++ii) { 912 | var functionName = creationFunctions[ii]; 913 | wrappedContext_[functionName] = function(f) { 914 | return function() { 915 | loseContextIfTime(); 916 | if (contextLost_) { 917 | return null; 918 | } 919 | var obj = f.apply(ctx, arguments); 920 | obj.__webglDebugContextLostId__ = contextId_; 921 | resourceDb_.push(obj); 922 | return obj; 923 | }; 924 | }(ctx[functionName]); 925 | } 926 | 927 | var functionsThatShouldReturnNull = [ 928 | "getActiveAttrib", 929 | "getActiveUniform", 930 | "getBufferParameter", 931 | "getContextAttributes", 932 | "getAttachedShaders", 933 | "getFramebufferAttachmentParameter", 934 | "getParameter", 935 | "getProgramParameter", 936 | "getProgramInfoLog", 937 | "getRenderbufferParameter", 938 | "getShaderParameter", 939 | "getShaderInfoLog", 940 | "getShaderSource", 941 | "getTexParameter", 942 | "getUniform", 943 | "getUniformLocation", 944 | "getVertexAttrib" 945 | ]; 946 | if (isWebGL2RenderingContext) { 947 | functionsThatShouldReturnNull.push( 948 | "getInternalformatParameter", 949 | "getQuery", 950 | "getQueryParameter", 951 | "getSamplerParameter", 952 | "getSyncParameter", 953 | "getTransformFeedbackVarying", 954 | "getIndexedParameter", 955 | "getUniformIndices", 956 | "getActiveUniforms", 957 | "getActiveUniformBlockParameter", 958 | "getActiveUniformBlockName" 959 | ); 960 | } 961 | for (var ii = 0; ii < functionsThatShouldReturnNull.length; ++ii) { 962 | var functionName = functionsThatShouldReturnNull[ii]; 963 | wrappedContext_[functionName] = function(f) { 964 | return function() { 965 | loseContextIfTime(); 966 | if (contextLost_) { 967 | return null; 968 | } 969 | return f.apply(ctx, arguments); 970 | } 971 | }(wrappedContext_[functionName]); 972 | } 973 | 974 | var isFunctions = [ 975 | "isBuffer", 976 | "isEnabled", 977 | "isFramebuffer", 978 | "isProgram", 979 | "isRenderbuffer", 980 | "isShader", 981 | "isTexture" 982 | ]; 983 | if (isWebGL2RenderingContext) { 984 | isFunctions.push( 985 | "isQuery", 986 | "isSampler", 987 | "isSync", 988 | "isTransformFeedback", 989 | "isVertexArray" 990 | ); 991 | } 992 | for (var ii = 0; ii < isFunctions.length; ++ii) { 993 | var functionName = isFunctions[ii]; 994 | wrappedContext_[functionName] = function(f) { 995 | return function() { 996 | loseContextIfTime(); 997 | if (contextLost_) { 998 | return false; 999 | } 1000 | return f.apply(ctx, arguments); 1001 | } 1002 | }(wrappedContext_[functionName]); 1003 | } 1004 | 1005 | wrappedContext_.checkFramebufferStatus = function(f) { 1006 | return function() { 1007 | loseContextIfTime(); 1008 | if (contextLost_) { 1009 | return wrappedContext_.FRAMEBUFFER_UNSUPPORTED; 1010 | } 1011 | return f.apply(ctx, arguments); 1012 | }; 1013 | }(wrappedContext_.checkFramebufferStatus); 1014 | 1015 | wrappedContext_.getAttribLocation = function(f) { 1016 | return function() { 1017 | loseContextIfTime(); 1018 | if (contextLost_) { 1019 | return -1; 1020 | } 1021 | return f.apply(ctx, arguments); 1022 | }; 1023 | }(wrappedContext_.getAttribLocation); 1024 | 1025 | wrappedContext_.getVertexAttribOffset = function(f) { 1026 | return function() { 1027 | loseContextIfTime(); 1028 | if (contextLost_) { 1029 | return 0; 1030 | } 1031 | return f.apply(ctx, arguments); 1032 | }; 1033 | }(wrappedContext_.getVertexAttribOffset); 1034 | 1035 | wrappedContext_.isContextLost = function() { 1036 | return contextLost_; 1037 | }; 1038 | 1039 | if (isWebGL2RenderingContext) { 1040 | wrappedContext_.getFragDataLocation = function(f) { 1041 | return function() { 1042 | loseContextIfTime(); 1043 | if (contextLost_) { 1044 | return -1; 1045 | } 1046 | return f.apply(ctx, arguments); 1047 | }; 1048 | }(wrappedContext_.getFragDataLocation); 1049 | 1050 | wrappedContext_.clientWaitSync = function(f) { 1051 | return function() { 1052 | loseContextIfTime(); 1053 | if (contextLost_) { 1054 | return wrappedContext_.WAIT_FAILED; 1055 | } 1056 | return f.apply(ctx, arguments); 1057 | }; 1058 | }(wrappedContext_.clientWaitSync); 1059 | 1060 | wrappedContext_.getUniformBlockIndex = function(f) { 1061 | return function() { 1062 | loseContextIfTime(); 1063 | if (contextLost_) { 1064 | return wrappedContext_.INVALID_INDEX; 1065 | } 1066 | return f.apply(ctx, arguments); 1067 | }; 1068 | }(wrappedContext_.getUniformBlockIndex); 1069 | } 1070 | 1071 | return wrappedContext_; 1072 | } 1073 | } 1074 | 1075 | return { 1076 | /** 1077 | * Initializes this module. Safe to call more than once. 1078 | * @param {!WebGLRenderingContext} ctx A WebGL context. If 1079 | * you have more than one context it doesn't matter which one 1080 | * you pass in, it is only used to pull out constants. 1081 | */ 1082 | 'init': init, 1083 | 1084 | /** 1085 | * Returns true or false if value matches any WebGL enum 1086 | * @param {*} value Value to check if it might be an enum. 1087 | * @return {boolean} True if value matches one of the WebGL defined enums 1088 | */ 1089 | 'mightBeEnum': mightBeEnum, 1090 | 1091 | /** 1092 | * Gets an string version of an WebGL enum. 1093 | * 1094 | * Example: 1095 | * WebGLDebugUtil.init(ctx); 1096 | * var str = WebGLDebugUtil.glEnumToString(ctx.getError()); 1097 | * 1098 | * @param {number} value Value to return an enum for 1099 | * @return {string} The string version of the enum. 1100 | */ 1101 | 'glEnumToString': glEnumToString, 1102 | 1103 | /** 1104 | * Converts the argument of a WebGL function to a string. 1105 | * Attempts to convert enum arguments to strings. 1106 | * 1107 | * Example: 1108 | * WebGLDebugUtil.init(ctx); 1109 | * var str = WebGLDebugUtil.glFunctionArgToString('bindTexture', 2, 0, gl.TEXTURE_2D); 1110 | * 1111 | * would return 'TEXTURE_2D' 1112 | * 1113 | * @param {string} functionName the name of the WebGL function. 1114 | * @param {number} numArgs The number of arguments 1115 | * @param {number} argumentIndx the index of the argument. 1116 | * @param {*} value The value of the argument. 1117 | * @return {string} The value as a string. 1118 | */ 1119 | 'glFunctionArgToString': glFunctionArgToString, 1120 | 1121 | /** 1122 | * Converts the arguments of a WebGL function to a string. 1123 | * Attempts to convert enum arguments to strings. 1124 | * 1125 | * @param {string} functionName the name of the WebGL function. 1126 | * @param {number} args The arguments. 1127 | * @return {string} The arguments as a string. 1128 | */ 1129 | 'glFunctionArgsToString': glFunctionArgsToString, 1130 | 1131 | /** 1132 | * Given a WebGL context returns a wrapped context that calls 1133 | * gl.getError after every command and calls a function if the 1134 | * result is not NO_ERROR. 1135 | * 1136 | * You can supply your own function if you want. For example, if you'd like 1137 | * an exception thrown on any GL error you could do this 1138 | * 1139 | * function throwOnGLError(err, funcName, args) { 1140 | * throw WebGLDebugUtils.glEnumToString(err) + 1141 | * " was caused by call to " + funcName; 1142 | * }; 1143 | * 1144 | * ctx = WebGLDebugUtils.makeDebugContext( 1145 | * canvas.getContext("webgl"), throwOnGLError); 1146 | * 1147 | * @param {!WebGLRenderingContext} ctx The webgl context to wrap. 1148 | * @param {!function(err, funcName, args): void} opt_onErrorFunc The function 1149 | * to call when gl.getError returns an error. If not specified the default 1150 | * function calls console.log with a message. 1151 | * @param {!function(funcName, args): void} opt_onFunc The 1152 | * function to call when each webgl function is called. You 1153 | * can use this to log all calls for example. 1154 | */ 1155 | 'makeDebugContext': makeDebugContext, 1156 | 1157 | /** 1158 | * Given a canvas element returns a wrapped canvas element that will 1159 | * simulate lost context. The canvas returned adds the following functions. 1160 | * 1161 | * loseContext: 1162 | * simulates a lost context event. 1163 | * 1164 | * restoreContext: 1165 | * simulates the context being restored. 1166 | * 1167 | * lostContextInNCalls: 1168 | * loses the context after N gl calls. 1169 | * 1170 | * getNumCalls: 1171 | * tells you how many gl calls there have been so far. 1172 | * 1173 | * setRestoreTimeout: 1174 | * sets the number of milliseconds until the context is restored 1175 | * after it has been lost. Defaults to 0. Pass -1 to prevent 1176 | * automatic restoring. 1177 | * 1178 | * @param {!Canvas} canvas The canvas element to wrap. 1179 | */ 1180 | 'makeLostContextSimulatingCanvas': makeLostContextSimulatingCanvas, 1181 | 1182 | /** 1183 | * Resets a context to the initial state. 1184 | * @param {!WebGLRenderingContext} ctx The webgl context to 1185 | * reset. 1186 | */ 1187 | 'resetToInitialState': resetToInitialState 1188 | }; 1189 | 1190 | }(); 1191 | 1192 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: 'mini-webgl.js', 9 | libraryTarget: 'umd', 10 | library: 'MiniWebGL', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.js/, exclude: /node_modules/, use: ['babel-loader'] }, 15 | { test: /\.(vs|fs)$/, exclude: /node_modules/, use: ['raw-loader', 'glslify-loader'] }, 16 | ] 17 | }, 18 | resolve: { 19 | extensions: ['.js'] 20 | }, 21 | watch: true, 22 | }; 23 | --------------------------------------------------------------------------------