├── OES_texture_float_linear-polyfill.js ├── README.md ├── cubemap.js ├── index.html ├── lightgl.js ├── main.js ├── renderer.js ├── tiles.jpg ├── water.js ├── xneg.jpg ├── xpos.jpg ├── ypos.jpg ├── zneg.jpg └── zpos.jpg /OES_texture_float_linear-polyfill.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Uploads a 2x2 floating-point texture where one pixel is 2 and the other 3 | // three pixels are 0. Linear filtering is only supported if a sample taken 4 | // from the center of that texture is (2 + 0 + 0 + 0) / 4 = 0.5. 5 | function supportsOESTextureFloatLinear(gl) { 6 | // Need floating point textures in the first place 7 | if (!gl.getExtension('OES_texture_float')) { 8 | return false; 9 | } 10 | 11 | // Create a render target 12 | var framebuffer = gl.createFramebuffer(); 13 | var byteTexture = gl.createTexture(); 14 | gl.bindTexture(gl.TEXTURE_2D, byteTexture); 15 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 16 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 17 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 18 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 19 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 20 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 21 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, byteTexture, 0); 22 | 23 | // Create a simple floating-point texture with value of 0.5 in the center 24 | var rgba = [ 25 | 2, 0, 0, 0, 26 | 0, 0, 0, 0, 27 | 0, 0, 0, 0, 28 | 0, 0, 0, 0 29 | ]; 30 | var floatTexture = gl.createTexture(); 31 | gl.bindTexture(gl.TEXTURE_2D, floatTexture); 32 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 33 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 34 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 35 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 36 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.FLOAT, new Float32Array(rgba)); 37 | 38 | // Create the test shader 39 | var program = gl.createProgram(); 40 | var vertexShader = gl.createShader(gl.VERTEX_SHADER); 41 | var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 42 | gl.shaderSource(vertexShader, '\ 43 | attribute vec2 vertex;\ 44 | void main() {\ 45 | gl_Position = vec4(vertex, 0.0, 1.0);\ 46 | }\ 47 | '); 48 | gl.shaderSource(fragmentShader, '\ 49 | uniform sampler2D texture;\ 50 | void main() {\ 51 | gl_FragColor = texture2D(texture, vec2(0.5));\ 52 | }\ 53 | '); 54 | gl.compileShader(vertexShader); 55 | gl.compileShader(fragmentShader); 56 | gl.attachShader(program, vertexShader); 57 | gl.attachShader(program, fragmentShader); 58 | gl.linkProgram(program); 59 | 60 | // Create a buffer containing a single point 61 | var buffer = gl.createBuffer(); 62 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 63 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0]), gl.STREAM_DRAW); 64 | gl.enableVertexAttribArray(0); 65 | gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 66 | 67 | // Render the point and read back the rendered pixel 68 | var pixel = new Uint8Array(4); 69 | gl.useProgram(program); 70 | gl.viewport(0, 0, 1, 1); 71 | gl.bindTexture(gl.TEXTURE_2D, floatTexture); 72 | gl.drawArrays(gl.POINTS, 0, 1); 73 | gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); 74 | 75 | // The center sample will only have a value of 0.5 if linear filtering works 76 | return pixel[0] === 127 || pixel[0] === 128; 77 | } 78 | 79 | // The constructor for the returned extension object 80 | function OESTextureFloatLinear() { 81 | } 82 | 83 | // Cache the extension so it's specific to each context like extensions should be 84 | function getOESTextureFloatLinear(gl) { 85 | if (gl.$OES_texture_float_linear$ === void 0) { 86 | Object.defineProperty(gl, '$OES_texture_float_linear$', { 87 | enumerable: false, 88 | configurable: false, 89 | writable: false, 90 | value: new OESTextureFloatLinear() 91 | }); 92 | } 93 | return gl.$OES_texture_float_linear$; 94 | } 95 | 96 | // This replaces the real getExtension() 97 | function getExtension(name) { 98 | return name === 'OES_texture_float_linear' 99 | ? getOESTextureFloatLinear(this) 100 | : oldGetExtension.call(this, name); 101 | } 102 | 103 | // This replaces the real getSupportedExtensions() 104 | function getSupportedExtensions() { 105 | var extensions = oldGetSupportedExtensions.call(this); 106 | if (extensions.indexOf('OES_texture_float_linear') === -1) { 107 | extensions.push('OES_texture_float_linear'); 108 | } 109 | return extensions; 110 | } 111 | 112 | // Get a WebGL context 113 | try { 114 | var gl = document.createElement('canvas').getContext('experimental-webgl'); 115 | } catch (e) { 116 | } 117 | 118 | // Don't install the polyfill if the browser already supports it or doesn't have WebGL 119 | if (!gl || gl.getSupportedExtensions().indexOf('OES_texture_float_linear') !== -1) { 120 | return; 121 | } 122 | 123 | // Install the polyfill if linear filtering works with floating-point textures 124 | if (supportsOESTextureFloatLinear(gl)) { 125 | var oldGetExtension = WebGLRenderingContext.prototype.getExtension; 126 | var oldGetSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions; 127 | WebGLRenderingContext.prototype.getExtension = getExtension; 128 | WebGLRenderingContext.prototype.getSupportedExtensions = getSupportedExtensions; 129 | } 130 | }()); 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL Water Demo 2 | 3 | http://madebyevan.com/webgl-water/ 4 | -------------------------------------------------------------------------------- /cubemap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WebGL Water 3 | * http://madebyevan.com/webgl-water/ 4 | * 5 | * Copyright 2011 Evan Wallace 6 | * Released under the MIT license 7 | */ 8 | 9 | function Cubemap(images) { 10 | this.id = gl.createTexture(); 11 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.id); 12 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 13 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 14 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 15 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 16 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 17 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.xneg); 18 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.xpos); 19 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.yneg); 20 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.ypos); 21 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.zneg); 22 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, images.zpos); 23 | } 24 | 25 | Cubemap.prototype.bind = function(unit) { 26 | gl.activeTexture(gl.TEXTURE0 + (unit || 0)); 27 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.id); 28 | }; 29 | 30 | Cubemap.prototype.unbind = function(unit) { 31 | gl.activeTexture(gl.TEXTURE0 + (unit || 0)); 32 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, null); 33 | }; 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | WebGL Water 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 35 | 36 |
Loading...
37 |
38 |

WebGL Water

39 |

Made by Evan Wallace

40 |

This demo requires a decent graphics card and up-to-date drivers. If you can't run the demo, you can still see it on YouTube.

41 |

Interactions:

42 | 50 |

Features:

51 | 58 |

* requires the OES_texture_float extension
** requires the OES_standard_derivatives extension

59 |

Tile texture from zooboing on Flickr

60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /lightgl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lightgl.js 3 | * http://github.com/evanw/lightgl.js/ 4 | * 5 | * Copyright 2011 Evan Wallace 6 | * Released under the MIT license 7 | */ 8 | var GL = (function() { 9 | 10 | // src/main.js 11 | // The internal `gl` variable holds the current WebGL context. 12 | var gl; 13 | 14 | var GL = { 15 | // ### Initialization 16 | // 17 | // `GL.create()` creates a new WebGL context and augments it with more 18 | // methods. The alpha channel is disabled by default because it usually causes 19 | // unintended transparencies in the canvas. 20 | create: function(options) { 21 | options = options || {}; 22 | var canvas = document.createElement('canvas'); 23 | canvas.width = 800; 24 | canvas.height = 600; 25 | if (!('alpha' in options)) options.alpha = false; 26 | try { gl = canvas.getContext('webgl', options); } catch (e) {} 27 | try { gl = gl || canvas.getContext('experimental-webgl', options); } catch (e) {} 28 | if (!gl) throw new Error('WebGL not supported'); 29 | gl.HALF_FLOAT_OES = 0x8D61; 30 | addMatrixStack(); 31 | addImmediateMode(); 32 | addEventListeners(); 33 | addOtherMethods(); 34 | return gl; 35 | }, 36 | 37 | // `GL.keys` contains a mapping of key codes to booleans indicating whether 38 | // that key is currently pressed. 39 | keys: {}, 40 | 41 | // Export all external classes. 42 | Matrix: Matrix, 43 | Indexer: Indexer, 44 | Buffer: Buffer, 45 | Mesh: Mesh, 46 | HitTest: HitTest, 47 | Raytracer: Raytracer, 48 | Shader: Shader, 49 | Texture: Texture, 50 | Vector: Vector 51 | }; 52 | 53 | // ### Matrix stack 54 | // 55 | // Implement the OpenGL modelview and projection matrix stacks, along with some 56 | // other useful GLU matrix functions. 57 | 58 | function addMatrixStack() { 59 | gl.MODELVIEW = ENUM | 1; 60 | gl.PROJECTION = ENUM | 2; 61 | var tempMatrix = new Matrix(); 62 | var resultMatrix = new Matrix(); 63 | gl.modelviewMatrix = new Matrix(); 64 | gl.projectionMatrix = new Matrix(); 65 | var modelviewStack = []; 66 | var projectionStack = []; 67 | var matrix, stack; 68 | gl.matrixMode = function(mode) { 69 | switch (mode) { 70 | case gl.MODELVIEW: 71 | matrix = 'modelviewMatrix'; 72 | stack = modelviewStack; 73 | break; 74 | case gl.PROJECTION: 75 | matrix = 'projectionMatrix'; 76 | stack = projectionStack; 77 | break; 78 | default: 79 | throw new Error('invalid matrix mode ' + mode); 80 | } 81 | }; 82 | gl.loadIdentity = function() { 83 | Matrix.identity(gl[matrix]); 84 | }; 85 | gl.loadMatrix = function(m) { 86 | var from = m.m, to = gl[matrix].m; 87 | for (var i = 0; i < 16; i++) { 88 | to[i] = from[i]; 89 | } 90 | }; 91 | gl.multMatrix = function(m) { 92 | gl.loadMatrix(Matrix.multiply(gl[matrix], m, resultMatrix)); 93 | }; 94 | gl.perspective = function(fov, aspect, near, far) { 95 | gl.multMatrix(Matrix.perspective(fov, aspect, near, far, tempMatrix)); 96 | }; 97 | gl.frustum = function(l, r, b, t, n, f) { 98 | gl.multMatrix(Matrix.frustum(l, r, b, t, n, f, tempMatrix)); 99 | }; 100 | gl.ortho = function(l, r, b, t, n, f) { 101 | gl.multMatrix(Matrix.ortho(l, r, b, t, n, f, tempMatrix)); 102 | }; 103 | gl.scale = function(x, y, z) { 104 | gl.multMatrix(Matrix.scale(x, y, z, tempMatrix)); 105 | }; 106 | gl.translate = function(x, y, z) { 107 | gl.multMatrix(Matrix.translate(x, y, z, tempMatrix)); 108 | }; 109 | gl.rotate = function(a, x, y, z) { 110 | gl.multMatrix(Matrix.rotate(a, x, y, z, tempMatrix)); 111 | }; 112 | gl.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz) { 113 | gl.multMatrix(Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz, tempMatrix)); 114 | }; 115 | gl.pushMatrix = function() { 116 | stack.push(Array.prototype.slice.call(gl[matrix].m)); 117 | }; 118 | gl.popMatrix = function() { 119 | var m = stack.pop(); 120 | gl[matrix].m = hasFloat32Array ? new Float32Array(m) : m; 121 | }; 122 | gl.project = function(objX, objY, objZ, modelview, projection, viewport) { 123 | modelview = modelview || gl.modelviewMatrix; 124 | projection = projection || gl.projectionMatrix; 125 | viewport = viewport || gl.getParameter(gl.VIEWPORT); 126 | var point = projection.transformPoint(modelview.transformPoint(new Vector(objX, objY, objZ))); 127 | return new Vector( 128 | viewport[0] + viewport[2] * (point.x * 0.5 + 0.5), 129 | viewport[1] + viewport[3] * (point.y * 0.5 + 0.5), 130 | point.z * 0.5 + 0.5 131 | ); 132 | }; 133 | gl.unProject = function(winX, winY, winZ, modelview, projection, viewport) { 134 | modelview = modelview || gl.modelviewMatrix; 135 | projection = projection || gl.projectionMatrix; 136 | viewport = viewport || gl.getParameter(gl.VIEWPORT); 137 | var point = new Vector( 138 | (winX - viewport[0]) / viewport[2] * 2 - 1, 139 | (winY - viewport[1]) / viewport[3] * 2 - 1, 140 | winZ * 2 - 1 141 | ); 142 | return Matrix.inverse(Matrix.multiply(projection, modelview, tempMatrix), resultMatrix).transformPoint(point); 143 | }; 144 | gl.matrixMode(gl.MODELVIEW); 145 | } 146 | 147 | // ### Immediate mode 148 | // 149 | // Provide an implementation of OpenGL's deprecated immediate mode. This is 150 | // depricated for a reason: constantly re-specifying the geometry is a bad 151 | // idea for performance. You should use a `GL.Mesh` instead, which specifies 152 | // the geometry once and caches it on the graphics card. Still, nothing 153 | // beats a quick `gl.begin(gl.POINTS); gl.vertex(1, 2, 3); gl.end();` for 154 | // debugging. This intentionally doesn't implement fixed-function lighting 155 | // because it's only meant for quick debugging tasks. 156 | 157 | function addImmediateMode() { 158 | var immediateMode = { 159 | mesh: new Mesh({ coords: true, colors: true, triangles: false }), 160 | mode: -1, 161 | coord: [0, 0, 0, 0], 162 | color: [1, 1, 1, 1], 163 | pointSize: 1, 164 | shader: new Shader('\ 165 | uniform float pointSize;\ 166 | varying vec4 color;\ 167 | varying vec4 coord;\ 168 | void main() {\ 169 | color = gl_Color;\ 170 | coord = gl_TexCoord;\ 171 | gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\ 172 | gl_PointSize = pointSize;\ 173 | }\ 174 | ', '\ 175 | uniform sampler2D texture;\ 176 | uniform float pointSize;\ 177 | uniform bool useTexture;\ 178 | varying vec4 color;\ 179 | varying vec4 coord;\ 180 | void main() {\ 181 | gl_FragColor = color;\ 182 | if (useTexture) gl_FragColor *= texture2D(texture, coord.xy);\ 183 | }\ 184 | ') 185 | }; 186 | gl.pointSize = function(pointSize) { 187 | immediateMode.shader.uniforms({ pointSize: pointSize }); 188 | }; 189 | gl.begin = function(mode) { 190 | if (immediateMode.mode != -1) throw new Error('mismatched gl.begin() and gl.end() calls'); 191 | immediateMode.mode = mode; 192 | immediateMode.mesh.colors = []; 193 | immediateMode.mesh.coords = []; 194 | immediateMode.mesh.vertices = []; 195 | }; 196 | gl.color = function(r, g, b, a) { 197 | immediateMode.color = (arguments.length == 1) ? r.toArray().concat(1) : [r, g, b, a || 1]; 198 | }; 199 | gl.texCoord = function(s, t) { 200 | immediateMode.coord = (arguments.length == 1) ? s.toArray(2) : [s, t]; 201 | }; 202 | gl.vertex = function(x, y, z) { 203 | immediateMode.mesh.colors.push(immediateMode.color); 204 | immediateMode.mesh.coords.push(immediateMode.coord); 205 | immediateMode.mesh.vertices.push(arguments.length == 1 ? x.toArray() : [x, y, z]); 206 | }; 207 | gl.end = function() { 208 | if (immediateMode.mode == -1) throw new Error('mismatched gl.begin() and gl.end() calls'); 209 | immediateMode.mesh.compile(); 210 | immediateMode.shader.uniforms({ 211 | useTexture: !!gl.getParameter(gl.TEXTURE_BINDING_2D) 212 | }).draw(immediateMode.mesh, immediateMode.mode); 213 | immediateMode.mode = -1; 214 | }; 215 | } 216 | 217 | // ### Improved mouse events 218 | // 219 | // This adds event listeners on the `gl.canvas` element that call 220 | // `gl.onmousedown()`, `gl.onmousemove()`, and `gl.onmouseup()` with an 221 | // augmented event object. The event object also has the properties `x`, `y`, 222 | // `deltaX`, `deltaY`, and `dragging`. 223 | function addEventListeners() { 224 | var context = gl, oldX = 0, oldY = 0, buttons = {}, hasOld = false; 225 | var has = Object.prototype.hasOwnProperty; 226 | function isDragging() { 227 | for (var b in buttons) { 228 | if (has.call(buttons, b) && buttons[b]) return true; 229 | } 230 | return false; 231 | } 232 | function augment(original) { 233 | // Make a copy of original, a native `MouseEvent`, so we can overwrite 234 | // WebKit's non-standard read-only `x` and `y` properties (which are just 235 | // duplicates of `pageX` and `pageY`). We can't just use 236 | // `Object.create(original)` because some `MouseEvent` functions must be 237 | // called in the context of the original event object. 238 | var e = {}; 239 | for (var name in original) { 240 | if (typeof original[name] == 'function') { 241 | e[name] = (function(callback) { 242 | return function() { 243 | callback.apply(original, arguments); 244 | }; 245 | })(original[name]); 246 | } else { 247 | e[name] = original[name]; 248 | } 249 | } 250 | e.original = original; 251 | e.x = e.pageX; 252 | e.y = e.pageY; 253 | for (var obj = gl.canvas; obj; obj = obj.offsetParent) { 254 | e.x -= obj.offsetLeft; 255 | e.y -= obj.offsetTop; 256 | } 257 | if (hasOld) { 258 | e.deltaX = e.x - oldX; 259 | e.deltaY = e.y - oldY; 260 | } else { 261 | e.deltaX = 0; 262 | e.deltaY = 0; 263 | hasOld = true; 264 | } 265 | oldX = e.x; 266 | oldY = e.y; 267 | e.dragging = isDragging(); 268 | e.preventDefault = function() { 269 | e.original.preventDefault(); 270 | }; 271 | e.stopPropagation = function() { 272 | e.original.stopPropagation(); 273 | }; 274 | return e; 275 | } 276 | function mousedown(e) { 277 | gl = context; 278 | if (!isDragging()) { 279 | // Expand the event handlers to the document to handle dragging off canvas. 280 | on(document, 'mousemove', mousemove); 281 | on(document, 'mouseup', mouseup); 282 | off(gl.canvas, 'mousemove', mousemove); 283 | off(gl.canvas, 'mouseup', mouseup); 284 | } 285 | buttons[e.which] = true; 286 | e = augment(e); 287 | if (gl.onmousedown) gl.onmousedown(e); 288 | e.preventDefault(); 289 | } 290 | function mousemove(e) { 291 | gl = context; 292 | e = augment(e); 293 | if (gl.onmousemove) gl.onmousemove(e); 294 | e.preventDefault(); 295 | } 296 | function mouseup(e) { 297 | gl = context; 298 | buttons[e.which] = false; 299 | if (!isDragging()) { 300 | // Shrink the event handlers back to the canvas when dragging ends. 301 | off(document, 'mousemove', mousemove); 302 | off(document, 'mouseup', mouseup); 303 | on(gl.canvas, 'mousemove', mousemove); 304 | on(gl.canvas, 'mouseup', mouseup); 305 | } 306 | e = augment(e); 307 | if (gl.onmouseup) gl.onmouseup(e); 308 | e.preventDefault(); 309 | } 310 | function reset() { 311 | hasOld = false; 312 | } 313 | function resetAll() { 314 | buttons = {}; 315 | hasOld = false; 316 | } 317 | on(gl.canvas, 'mousedown', mousedown); 318 | on(gl.canvas, 'mousemove', mousemove); 319 | on(gl.canvas, 'mouseup', mouseup); 320 | on(gl.canvas, 'mouseover', reset); 321 | on(gl.canvas, 'mouseout', reset); 322 | on(document, 'contextmenu', resetAll); 323 | } 324 | 325 | // ### Automatic keyboard state 326 | // 327 | // The current keyboard state is stored in `GL.keys`, a map of integer key 328 | // codes to booleans indicating whether that key is currently pressed. Certain 329 | // keys also have named identifiers that can be used directly, such as 330 | // `GL.keys.SPACE`. Values in `GL.keys` are initially undefined until that 331 | // key is pressed for the first time. If you need a boolean value, you can 332 | // cast the value to boolean by applying the not operator twice (as in 333 | // `!!GL.keys.SPACE`). 334 | 335 | function mapKeyCode(code) { 336 | var named = { 337 | 8: 'BACKSPACE', 338 | 9: 'TAB', 339 | 13: 'ENTER', 340 | 16: 'SHIFT', 341 | 27: 'ESCAPE', 342 | 32: 'SPACE', 343 | 37: 'LEFT', 344 | 38: 'UP', 345 | 39: 'RIGHT', 346 | 40: 'DOWN' 347 | }; 348 | return named[code] || (code >= 65 && code <= 90 ? String.fromCharCode(code) : null); 349 | } 350 | 351 | function on(element, name, callback) { 352 | element.addEventListener(name, callback); 353 | } 354 | 355 | function off(element, name, callback) { 356 | element.removeEventListener(name, callback); 357 | } 358 | 359 | on(document, 'keydown', function(e) { 360 | if (!e.altKey && !e.ctrlKey && !e.metaKey) { 361 | var key = mapKeyCode(e.keyCode); 362 | if (key) GL.keys[key] = true; 363 | GL.keys[e.keyCode] = true; 364 | } 365 | }); 366 | 367 | on(document, 'keyup', function(e) { 368 | if (!e.altKey && !e.ctrlKey && !e.metaKey) { 369 | var key = mapKeyCode(e.keyCode); 370 | if (key) GL.keys[key] = false; 371 | GL.keys[e.keyCode] = false; 372 | } 373 | }); 374 | 375 | function addOtherMethods() { 376 | // ### Multiple contexts 377 | // 378 | // When using multiple contexts in one web page, `gl.makeCurrent()` must be 379 | // called before issuing commands to a different context. 380 | (function(context) { 381 | gl.makeCurrent = function() { 382 | gl = context; 383 | }; 384 | })(gl); 385 | 386 | // ### Animation 387 | // 388 | // Call `gl.animate()` to provide an animation loop that repeatedly calls 389 | // `gl.onupdate()` and `gl.ondraw()`. 390 | gl.animate = function() { 391 | var post = 392 | window.requestAnimationFrame || 393 | window.mozRequestAnimationFrame || 394 | window.webkitRequestAnimationFrame || 395 | function(callback) { setTimeout(callback, 1000 / 60); }; 396 | var time = new Date().getTime(); 397 | var context = gl; 398 | function update() { 399 | gl = context; 400 | var now = new Date().getTime(); 401 | if (gl.onupdate) gl.onupdate((now - time) / 1000); 402 | if (gl.ondraw) gl.ondraw(); 403 | post(update); 404 | time = now; 405 | } 406 | update(); 407 | }; 408 | 409 | // ### Fullscreen 410 | // 411 | // Provide an easy way to get a fullscreen app running, including an 412 | // automatic 3D perspective projection matrix by default. This should be 413 | // called once. 414 | // 415 | // Just fullscreen, no automatic camera: 416 | // 417 | // gl.fullscreen({ camera: false }); 418 | // 419 | // Adjusting field of view, near plane distance, and far plane distance: 420 | // 421 | // gl.fullscreen({ fov: 45, near: 0.1, far: 1000 }); 422 | // 423 | // Adding padding from the edge of the window: 424 | // 425 | // gl.fullscreen({ paddingLeft: 250, paddingBottom: 60 }); 426 | // 427 | gl.fullscreen = function(options) { 428 | options = options || {}; 429 | var top = options.paddingTop || 0; 430 | var left = options.paddingLeft || 0; 431 | var right = options.paddingRight || 0; 432 | var bottom = options.paddingBottom || 0; 433 | if (!document.body) { 434 | throw new Error('document.body doesn\'t exist yet (call gl.fullscreen() from ' + 435 | 'window.onload() or from inside the tag)'); 436 | } 437 | document.body.appendChild(gl.canvas); 438 | document.body.style.overflow = 'hidden'; 439 | gl.canvas.style.position = 'absolute'; 440 | gl.canvas.style.left = left + 'px'; 441 | gl.canvas.style.top = top + 'px'; 442 | function resize() { 443 | gl.canvas.width = window.innerWidth - left - right; 444 | gl.canvas.height = window.innerHeight - top - bottom; 445 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 446 | if (options.camera || !('camera' in options)) { 447 | gl.matrixMode(gl.PROJECTION); 448 | gl.loadIdentity(); 449 | gl.perspective(options.fov || 45, gl.canvas.width / gl.canvas.height, 450 | options.near || 0.1, options.far || 1000); 451 | gl.matrixMode(gl.MODELVIEW); 452 | } 453 | if (gl.ondraw) gl.ondraw(); 454 | } 455 | on(window, 'resize', resize); 456 | resize(); 457 | }; 458 | } 459 | 460 | // A value to bitwise-or with new enums to make them distinguishable from the 461 | // standard WebGL enums. 462 | var ENUM = 0x12340000; 463 | 464 | // src/matrix.js 465 | // Represents a 4x4 matrix stored in row-major order that uses Float32Arrays 466 | // when available. Matrix operations can either be done using convenient 467 | // methods that return a new matrix for the result or optimized methods 468 | // that store the result in an existing matrix to avoid generating garbage. 469 | 470 | var hasFloat32Array = (typeof Float32Array != 'undefined'); 471 | 472 | // ### new GL.Matrix([elements]) 473 | // 474 | // This constructor takes 16 arguments in row-major order, which can be passed 475 | // individually, as a list, or even as four lists, one for each row. If the 476 | // arguments are omitted then the identity matrix is constructed instead. 477 | function Matrix() { 478 | var m = Array.prototype.concat.apply([], arguments); 479 | if (!m.length) { 480 | m = [ 481 | 1, 0, 0, 0, 482 | 0, 1, 0, 0, 483 | 0, 0, 1, 0, 484 | 0, 0, 0, 1 485 | ]; 486 | } 487 | this.m = hasFloat32Array ? new Float32Array(m) : m; 488 | } 489 | 490 | Matrix.prototype = { 491 | // ### .inverse() 492 | // 493 | // Returns the matrix that when multiplied with this matrix results in the 494 | // identity matrix. 495 | inverse: function() { 496 | return Matrix.inverse(this, new Matrix()); 497 | }, 498 | 499 | // ### .transpose() 500 | // 501 | // Returns this matrix, exchanging columns for rows. 502 | transpose: function() { 503 | return Matrix.transpose(this, new Matrix()); 504 | }, 505 | 506 | // ### .multiply(matrix) 507 | // 508 | // Returns the concatenation of the transforms for this matrix and `matrix`. 509 | // This emulates the OpenGL function `glMultMatrix()`. 510 | multiply: function(matrix) { 511 | return Matrix.multiply(this, matrix, new Matrix()); 512 | }, 513 | 514 | // ### .transformPoint(point) 515 | // 516 | // Transforms the vector as a point with a w coordinate of 1. This 517 | // means translations will have an effect, for example. 518 | transformPoint: function(v) { 519 | var m = this.m; 520 | return new Vector( 521 | m[0] * v.x + m[1] * v.y + m[2] * v.z + m[3], 522 | m[4] * v.x + m[5] * v.y + m[6] * v.z + m[7], 523 | m[8] * v.x + m[9] * v.y + m[10] * v.z + m[11] 524 | ).divide(m[12] * v.x + m[13] * v.y + m[14] * v.z + m[15]); 525 | }, 526 | 527 | // ### .transformPoint(vector) 528 | // 529 | // Transforms the vector as a vector with a w coordinate of 0. This 530 | // means translations will have no effect, for example. 531 | transformVector: function(v) { 532 | var m = this.m; 533 | return new Vector( 534 | m[0] * v.x + m[1] * v.y + m[2] * v.z, 535 | m[4] * v.x + m[5] * v.y + m[6] * v.z, 536 | m[8] * v.x + m[9] * v.y + m[10] * v.z 537 | ); 538 | } 539 | }; 540 | 541 | // ### GL.Matrix.inverse(matrix[, result]) 542 | // 543 | // Returns the matrix that when multiplied with `matrix` results in the 544 | // identity matrix. You can optionally pass an existing matrix in `result` 545 | // to avoid allocating a new matrix. This implementation is from the Mesa 546 | // OpenGL function `__gluInvertMatrixd()` found in `project.c`. 547 | Matrix.inverse = function(matrix, result) { 548 | result = result || new Matrix(); 549 | var m = matrix.m, r = result.m; 550 | 551 | r[0] = m[5]*m[10]*m[15] - m[5]*m[14]*m[11] - m[6]*m[9]*m[15] + m[6]*m[13]*m[11] + m[7]*m[9]*m[14] - m[7]*m[13]*m[10]; 552 | r[1] = -m[1]*m[10]*m[15] + m[1]*m[14]*m[11] + m[2]*m[9]*m[15] - m[2]*m[13]*m[11] - m[3]*m[9]*m[14] + m[3]*m[13]*m[10]; 553 | r[2] = m[1]*m[6]*m[15] - m[1]*m[14]*m[7] - m[2]*m[5]*m[15] + m[2]*m[13]*m[7] + m[3]*m[5]*m[14] - m[3]*m[13]*m[6]; 554 | r[3] = -m[1]*m[6]*m[11] + m[1]*m[10]*m[7] + m[2]*m[5]*m[11] - m[2]*m[9]*m[7] - m[3]*m[5]*m[10] + m[3]*m[9]*m[6]; 555 | 556 | r[4] = -m[4]*m[10]*m[15] + m[4]*m[14]*m[11] + m[6]*m[8]*m[15] - m[6]*m[12]*m[11] - m[7]*m[8]*m[14] + m[7]*m[12]*m[10]; 557 | r[5] = m[0]*m[10]*m[15] - m[0]*m[14]*m[11] - m[2]*m[8]*m[15] + m[2]*m[12]*m[11] + m[3]*m[8]*m[14] - m[3]*m[12]*m[10]; 558 | r[6] = -m[0]*m[6]*m[15] + m[0]*m[14]*m[7] + m[2]*m[4]*m[15] - m[2]*m[12]*m[7] - m[3]*m[4]*m[14] + m[3]*m[12]*m[6]; 559 | r[7] = m[0]*m[6]*m[11] - m[0]*m[10]*m[7] - m[2]*m[4]*m[11] + m[2]*m[8]*m[7] + m[3]*m[4]*m[10] - m[3]*m[8]*m[6]; 560 | 561 | r[8] = m[4]*m[9]*m[15] - m[4]*m[13]*m[11] - m[5]*m[8]*m[15] + m[5]*m[12]*m[11] + m[7]*m[8]*m[13] - m[7]*m[12]*m[9]; 562 | r[9] = -m[0]*m[9]*m[15] + m[0]*m[13]*m[11] + m[1]*m[8]*m[15] - m[1]*m[12]*m[11] - m[3]*m[8]*m[13] + m[3]*m[12]*m[9]; 563 | r[10] = m[0]*m[5]*m[15] - m[0]*m[13]*m[7] - m[1]*m[4]*m[15] + m[1]*m[12]*m[7] + m[3]*m[4]*m[13] - m[3]*m[12]*m[5]; 564 | r[11] = -m[0]*m[5]*m[11] + m[0]*m[9]*m[7] + m[1]*m[4]*m[11] - m[1]*m[8]*m[7] - m[3]*m[4]*m[9] + m[3]*m[8]*m[5]; 565 | 566 | r[12] = -m[4]*m[9]*m[14] + m[4]*m[13]*m[10] + m[5]*m[8]*m[14] - m[5]*m[12]*m[10] - m[6]*m[8]*m[13] + m[6]*m[12]*m[9]; 567 | r[13] = m[0]*m[9]*m[14] - m[0]*m[13]*m[10] - m[1]*m[8]*m[14] + m[1]*m[12]*m[10] + m[2]*m[8]*m[13] - m[2]*m[12]*m[9]; 568 | r[14] = -m[0]*m[5]*m[14] + m[0]*m[13]*m[6] + m[1]*m[4]*m[14] - m[1]*m[12]*m[6] - m[2]*m[4]*m[13] + m[2]*m[12]*m[5]; 569 | r[15] = m[0]*m[5]*m[10] - m[0]*m[9]*m[6] - m[1]*m[4]*m[10] + m[1]*m[8]*m[6] + m[2]*m[4]*m[9] - m[2]*m[8]*m[5]; 570 | 571 | var det = m[0]*r[0] + m[1]*r[4] + m[2]*r[8] + m[3]*r[12]; 572 | for (var i = 0; i < 16; i++) r[i] /= det; 573 | return result; 574 | }; 575 | 576 | // ### GL.Matrix.transpose(matrix[, result]) 577 | // 578 | // Returns `matrix`, exchanging columns for rows. You can optionally pass an 579 | // existing matrix in `result` to avoid allocating a new matrix. 580 | Matrix.transpose = function(matrix, result) { 581 | result = result || new Matrix(); 582 | var m = matrix.m, r = result.m; 583 | r[0] = m[0]; r[1] = m[4]; r[2] = m[8]; r[3] = m[12]; 584 | r[4] = m[1]; r[5] = m[5]; r[6] = m[9]; r[7] = m[13]; 585 | r[8] = m[2]; r[9] = m[6]; r[10] = m[10]; r[11] = m[14]; 586 | r[12] = m[3]; r[13] = m[7]; r[14] = m[11]; r[15] = m[15]; 587 | return result; 588 | }; 589 | 590 | // ### GL.Matrix.multiply(left, right[, result]) 591 | // 592 | // Returns the concatenation of the transforms for `left` and `right`. You can 593 | // optionally pass an existing matrix in `result` to avoid allocating a new 594 | // matrix. This emulates the OpenGL function `glMultMatrix()`. 595 | Matrix.multiply = function(left, right, result) { 596 | result = result || new Matrix(); 597 | var a = left.m, b = right.m, r = result.m; 598 | 599 | r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; 600 | r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; 601 | r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; 602 | r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; 603 | 604 | r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; 605 | r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; 606 | r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; 607 | r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; 608 | 609 | r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; 610 | r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; 611 | r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; 612 | r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; 613 | 614 | r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; 615 | r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; 616 | r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; 617 | r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; 618 | 619 | return result; 620 | }; 621 | 622 | // ### GL.Matrix.identity([result]) 623 | // 624 | // Returns an identity matrix. You can optionally pass an existing matrix in 625 | // `result` to avoid allocating a new matrix. This emulates the OpenGL function 626 | // `glLoadIdentity()`. 627 | Matrix.identity = function(result) { 628 | result = result || new Matrix(); 629 | var m = result.m; 630 | m[0] = m[5] = m[10] = m[15] = 1; 631 | m[1] = m[2] = m[3] = m[4] = m[6] = m[7] = m[8] = m[9] = m[11] = m[12] = m[13] = m[14] = 0; 632 | return result; 633 | }; 634 | 635 | // ### GL.Matrix.perspective(fov, aspect, near, far[, result]) 636 | // 637 | // Returns a perspective transform matrix, which makes far away objects appear 638 | // smaller than nearby objects. The `aspect` argument should be the width 639 | // divided by the height of your viewport and `fov` is the top-to-bottom angle 640 | // of the field of view in degrees. You can optionally pass an existing matrix 641 | // in `result` to avoid allocating a new matrix. This emulates the OpenGL 642 | // function `gluPerspective()`. 643 | Matrix.perspective = function(fov, aspect, near, far, result) { 644 | var y = Math.tan(fov * Math.PI / 360) * near; 645 | var x = y * aspect; 646 | return Matrix.frustum(-x, x, -y, y, near, far, result); 647 | }; 648 | 649 | // ### GL.Matrix.frustum(left, right, bottom, top, near, far[, result]) 650 | // 651 | // Sets up a viewing frustum, which is shaped like a truncated pyramid with the 652 | // camera where the point of the pyramid would be. You can optionally pass an 653 | // existing matrix in `result` to avoid allocating a new matrix. This emulates 654 | // the OpenGL function `glFrustum()`. 655 | Matrix.frustum = function(l, r, b, t, n, f, result) { 656 | result = result || new Matrix(); 657 | var m = result.m; 658 | 659 | m[0] = 2 * n / (r - l); 660 | m[1] = 0; 661 | m[2] = (r + l) / (r - l); 662 | m[3] = 0; 663 | 664 | m[4] = 0; 665 | m[5] = 2 * n / (t - b); 666 | m[6] = (t + b) / (t - b); 667 | m[7] = 0; 668 | 669 | m[8] = 0; 670 | m[9] = 0; 671 | m[10] = -(f + n) / (f - n); 672 | m[11] = -2 * f * n / (f - n); 673 | 674 | m[12] = 0; 675 | m[13] = 0; 676 | m[14] = -1; 677 | m[15] = 0; 678 | 679 | return result; 680 | }; 681 | 682 | // ### GL.Matrix.ortho(left, right, bottom, top, near, far[, result]) 683 | // 684 | // Returns an orthographic projection, in which objects are the same size no 685 | // matter how far away or nearby they are. You can optionally pass an existing 686 | // matrix in `result` to avoid allocating a new matrix. This emulates the OpenGL 687 | // function `glOrtho()`. 688 | Matrix.ortho = function(l, r, b, t, n, f, result) { 689 | result = result || new Matrix(); 690 | var m = result.m; 691 | 692 | m[0] = 2 / (r - l); 693 | m[1] = 0; 694 | m[2] = 0; 695 | m[3] = -(r + l) / (r - l); 696 | 697 | m[4] = 0; 698 | m[5] = 2 / (t - b); 699 | m[6] = 0; 700 | m[7] = -(t + b) / (t - b); 701 | 702 | m[8] = 0; 703 | m[9] = 0; 704 | m[10] = -2 / (f - n); 705 | m[11] = -(f + n) / (f - n); 706 | 707 | m[12] = 0; 708 | m[13] = 0; 709 | m[14] = 0; 710 | m[15] = 1; 711 | 712 | return result; 713 | }; 714 | 715 | // ### GL.Matrix.scale(x, y, z[, result]) 716 | // 717 | // This emulates the OpenGL function `glScale()`. You can optionally pass an 718 | // existing matrix in `result` to avoid allocating a new matrix. 719 | Matrix.scale = function(x, y, z, result) { 720 | result = result || new Matrix(); 721 | var m = result.m; 722 | 723 | m[0] = x; 724 | m[1] = 0; 725 | m[2] = 0; 726 | m[3] = 0; 727 | 728 | m[4] = 0; 729 | m[5] = y; 730 | m[6] = 0; 731 | m[7] = 0; 732 | 733 | m[8] = 0; 734 | m[9] = 0; 735 | m[10] = z; 736 | m[11] = 0; 737 | 738 | m[12] = 0; 739 | m[13] = 0; 740 | m[14] = 0; 741 | m[15] = 1; 742 | 743 | return result; 744 | }; 745 | 746 | // ### GL.Matrix.translate(x, y, z[, result]) 747 | // 748 | // This emulates the OpenGL function `glTranslate()`. You can optionally pass 749 | // an existing matrix in `result` to avoid allocating a new matrix. 750 | Matrix.translate = function(x, y, z, result) { 751 | result = result || new Matrix(); 752 | var m = result.m; 753 | 754 | m[0] = 1; 755 | m[1] = 0; 756 | m[2] = 0; 757 | m[3] = x; 758 | 759 | m[4] = 0; 760 | m[5] = 1; 761 | m[6] = 0; 762 | m[7] = y; 763 | 764 | m[8] = 0; 765 | m[9] = 0; 766 | m[10] = 1; 767 | m[11] = z; 768 | 769 | m[12] = 0; 770 | m[13] = 0; 771 | m[14] = 0; 772 | m[15] = 1; 773 | 774 | return result; 775 | }; 776 | 777 | // ### GL.Matrix.rotate(a, x, y, z[, result]) 778 | // 779 | // Returns a matrix that rotates by `a` degrees around the vector `x, y, z`. 780 | // You can optionally pass an existing matrix in `result` to avoid allocating 781 | // a new matrix. This emulates the OpenGL function `glRotate()`. 782 | Matrix.rotate = function(a, x, y, z, result) { 783 | if (!a || (!x && !y && !z)) { 784 | return Matrix.identity(result); 785 | } 786 | 787 | result = result || new Matrix(); 788 | var m = result.m; 789 | 790 | var d = Math.sqrt(x*x + y*y + z*z); 791 | a *= Math.PI / 180; x /= d; y /= d; z /= d; 792 | var c = Math.cos(a), s = Math.sin(a), t = 1 - c; 793 | 794 | m[0] = x * x * t + c; 795 | m[1] = x * y * t - z * s; 796 | m[2] = x * z * t + y * s; 797 | m[3] = 0; 798 | 799 | m[4] = y * x * t + z * s; 800 | m[5] = y * y * t + c; 801 | m[6] = y * z * t - x * s; 802 | m[7] = 0; 803 | 804 | m[8] = z * x * t - y * s; 805 | m[9] = z * y * t + x * s; 806 | m[10] = z * z * t + c; 807 | m[11] = 0; 808 | 809 | m[12] = 0; 810 | m[13] = 0; 811 | m[14] = 0; 812 | m[15] = 1; 813 | 814 | return result; 815 | }; 816 | 817 | // ### GL.Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz[, result]) 818 | // 819 | // Returns a matrix that puts the camera at the eye point `ex, ey, ez` looking 820 | // toward the center point `cx, cy, cz` with an up direction of `ux, uy, uz`. 821 | // You can optionally pass an existing matrix in `result` to avoid allocating 822 | // a new matrix. This emulates the OpenGL function `gluLookAt()`. 823 | Matrix.lookAt = function(ex, ey, ez, cx, cy, cz, ux, uy, uz, result) { 824 | result = result || new Matrix(); 825 | var m = result.m; 826 | 827 | var e = new Vector(ex, ey, ez); 828 | var c = new Vector(cx, cy, cz); 829 | var u = new Vector(ux, uy, uz); 830 | var f = e.subtract(c).unit(); 831 | var s = u.cross(f).unit(); 832 | var t = f.cross(s).unit(); 833 | 834 | m[0] = s.x; 835 | m[1] = s.y; 836 | m[2] = s.z; 837 | m[3] = -s.dot(e); 838 | 839 | m[4] = t.x; 840 | m[5] = t.y; 841 | m[6] = t.z; 842 | m[7] = -t.dot(e); 843 | 844 | m[8] = f.x; 845 | m[9] = f.y; 846 | m[10] = f.z; 847 | m[11] = -f.dot(e); 848 | 849 | m[12] = 0; 850 | m[13] = 0; 851 | m[14] = 0; 852 | m[15] = 1; 853 | 854 | return result; 855 | }; 856 | 857 | // src/mesh.js 858 | // Represents indexed triangle geometry with arbitrary additional attributes. 859 | // You need a shader to draw a mesh; meshes can't draw themselves. 860 | // 861 | // A mesh is a collection of `GL.Buffer` objects which are either vertex buffers 862 | // (holding per-vertex attributes) or index buffers (holding the order in which 863 | // vertices are rendered). By default, a mesh has a position vertex buffer called 864 | // `vertices` and a triangle index buffer called `triangles`. New buffers can be 865 | // added using `addVertexBuffer()` and `addIndexBuffer()`. Two strings are 866 | // required when adding a new vertex buffer, the name of the data array on the 867 | // mesh instance and the name of the GLSL attribute in the vertex shader. 868 | // 869 | // Example usage: 870 | // 871 | // var mesh = new GL.Mesh({ coords: true, lines: true }); 872 | // 873 | // // Default attribute "vertices", available as "gl_Vertex" in 874 | // // the vertex shader 875 | // mesh.vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]; 876 | // 877 | // // Optional attribute "coords" enabled in constructor, 878 | // // available as "gl_TexCoord" in the vertex shader 879 | // mesh.coords = [[0, 0], [1, 0], [0, 1], [1, 1]]; 880 | // 881 | // // Custom attribute "weights", available as "weight" in the 882 | // // vertex shader 883 | // mesh.addVertexBuffer('weights', 'weight'); 884 | // mesh.weights = [1, 0, 0, 1]; 885 | // 886 | // // Default index buffer "triangles" 887 | // mesh.triangles = [[0, 1, 2], [2, 1, 3]]; 888 | // 889 | // // Optional index buffer "lines" enabled in constructor 890 | // mesh.lines = [[0, 1], [0, 2], [1, 3], [2, 3]]; 891 | // 892 | // // Upload provided data to GPU memory 893 | // mesh.compile(); 894 | 895 | // ### new GL.Indexer() 896 | // 897 | // Generates indices into a list of unique objects from a stream of objects 898 | // that may contain duplicates. This is useful for generating compact indexed 899 | // meshes from unindexed data. 900 | function Indexer() { 901 | this.unique = []; 902 | this.indices = []; 903 | this.map = {}; 904 | } 905 | 906 | Indexer.prototype = { 907 | // ### .add(v) 908 | // 909 | // Adds the object `obj` to `unique` if it hasn't already been added. Returns 910 | // the index of `obj` in `unique`. 911 | add: function(obj) { 912 | var key = JSON.stringify(obj); 913 | if (!(key in this.map)) { 914 | this.map[key] = this.unique.length; 915 | this.unique.push(obj); 916 | } 917 | return this.map[key]; 918 | } 919 | }; 920 | 921 | // ### new GL.Buffer(target, type) 922 | // 923 | // Provides a simple method of uploading data to a GPU buffer. Example usage: 924 | // 925 | // var vertices = new GL.Buffer(gl.ARRAY_BUFFER, Float32Array); 926 | // var indices = new GL.Buffer(gl.ELEMENT_ARRAY_BUFFER, Uint16Array); 927 | // vertices.data = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]; 928 | // indices.data = [[0, 1, 2], [2, 1, 3]]; 929 | // vertices.compile(); 930 | // indices.compile(); 931 | // 932 | function Buffer(target, type) { 933 | this.buffer = null; 934 | this.target = target; 935 | this.type = type; 936 | this.data = []; 937 | } 938 | 939 | Buffer.prototype = { 940 | // ### .compile(type) 941 | // 942 | // Upload the contents of `data` to the GPU in preparation for rendering. The 943 | // data must be a list of lists where each inner list has the same length. For 944 | // example, each element of data for vertex normals would be a list of length three. 945 | // This will remember the data length and element length for later use by shaders. 946 | // The type can be either `gl.STATIC_DRAW` or `gl.DYNAMIC_DRAW`, and defaults to 947 | // `gl.STATIC_DRAW`. 948 | // 949 | // This could have used `[].concat.apply([], this.data)` to flatten 950 | // the array but Google Chrome has a maximum number of arguments so the 951 | // concatenations are chunked to avoid that limit. 952 | compile: function(type) { 953 | var data = []; 954 | for (var i = 0, chunk = 10000; i < this.data.length; i += chunk) { 955 | data = Array.prototype.concat.apply(data, this.data.slice(i, i + chunk)); 956 | } 957 | var spacing = this.data.length ? data.length / this.data.length : 0; 958 | if (spacing != Math.round(spacing)) throw new Error('buffer elements not of consistent size, average size is ' + spacing); 959 | this.buffer = this.buffer || gl.createBuffer(); 960 | this.buffer.length = data.length; 961 | this.buffer.spacing = spacing; 962 | gl.bindBuffer(this.target, this.buffer); 963 | gl.bufferData(this.target, new this.type(data), type || gl.STATIC_DRAW); 964 | } 965 | }; 966 | 967 | // ### new GL.Mesh([options]) 968 | // 969 | // Represents a collection of vertex buffers and index buffers. Each vertex 970 | // buffer maps to one attribute in GLSL and has a corresponding property set 971 | // on the Mesh instance. There is one vertex buffer by default: `vertices`, 972 | // which maps to `gl_Vertex`. The `coords`, `normals`, and `colors` vertex 973 | // buffers map to `gl_TexCoord`, `gl_Normal`, and `gl_Color` respectively, 974 | // and can be enabled by setting the corresponding options to true. There are 975 | // two index buffers, `triangles` and `lines`, which are used for rendering 976 | // `gl.TRIANGLES` and `gl.LINES`, respectively. Only `triangles` is enabled by 977 | // default, although `computeWireframe()` will add a normal buffer if it wasn't 978 | // initially enabled. 979 | function Mesh(options) { 980 | options = options || {}; 981 | this.vertexBuffers = {}; 982 | this.indexBuffers = {}; 983 | this.addVertexBuffer('vertices', 'gl_Vertex'); 984 | if (options.coords) this.addVertexBuffer('coords', 'gl_TexCoord'); 985 | if (options.normals) this.addVertexBuffer('normals', 'gl_Normal'); 986 | if (options.colors) this.addVertexBuffer('colors', 'gl_Color'); 987 | if (!('triangles' in options) || options.triangles) this.addIndexBuffer('triangles'); 988 | if (options.lines) this.addIndexBuffer('lines'); 989 | } 990 | 991 | Mesh.prototype = { 992 | // ### .addVertexBuffer(name, attribute) 993 | // 994 | // Add a new vertex buffer with a list as a property called `name` on this object 995 | // and map it to the attribute called `attribute` in all shaders that draw this mesh. 996 | addVertexBuffer: function(name, attribute) { 997 | var buffer = this.vertexBuffers[attribute] = new Buffer(gl.ARRAY_BUFFER, Float32Array); 998 | buffer.name = name; 999 | this[name] = []; 1000 | }, 1001 | 1002 | // ### .addIndexBuffer(name) 1003 | // 1004 | // Add a new index buffer with a list as a property called `name` on this object. 1005 | addIndexBuffer: function(name) { 1006 | var buffer = this.indexBuffers[name] = new Buffer(gl.ELEMENT_ARRAY_BUFFER, Uint16Array); 1007 | this[name] = []; 1008 | }, 1009 | 1010 | // ### .compile() 1011 | // 1012 | // Upload all attached buffers to the GPU in preparation for rendering. This 1013 | // doesn't need to be called every frame, only needs to be done when the data 1014 | // changes. 1015 | compile: function() { 1016 | for (var attribute in this.vertexBuffers) { 1017 | var buffer = this.vertexBuffers[attribute]; 1018 | buffer.data = this[buffer.name]; 1019 | buffer.compile(); 1020 | } 1021 | 1022 | for (var name in this.indexBuffers) { 1023 | var buffer = this.indexBuffers[name]; 1024 | buffer.data = this[name]; 1025 | buffer.compile(); 1026 | } 1027 | }, 1028 | 1029 | // ### .transform(matrix) 1030 | // 1031 | // Transform all vertices by `matrix` and all normals by the inverse transpose 1032 | // of `matrix`. 1033 | transform: function(matrix) { 1034 | this.vertices = this.vertices.map(function(v) { 1035 | return matrix.transformPoint(Vector.fromArray(v)).toArray(); 1036 | }); 1037 | if (this.normals) { 1038 | var invTrans = matrix.inverse().transpose(); 1039 | this.normals = this.normals.map(function(n) { 1040 | return invTrans.transformVector(Vector.fromArray(n)).unit().toArray(); 1041 | }); 1042 | } 1043 | this.compile(); 1044 | return this; 1045 | }, 1046 | 1047 | // ### .computeNormals() 1048 | // 1049 | // Computes a new normal for each vertex from the average normal of the 1050 | // neighboring triangles. This means adjacent triangles must share vertices 1051 | // for the resulting normals to be smooth. 1052 | computeNormals: function() { 1053 | if (!this.normals) this.addVertexBuffer('normals', 'gl_Normal'); 1054 | for (var i = 0; i < this.vertices.length; i++) { 1055 | this.normals[i] = new Vector(); 1056 | } 1057 | for (var i = 0; i < this.triangles.length; i++) { 1058 | var t = this.triangles[i]; 1059 | var a = Vector.fromArray(this.vertices[t[0]]); 1060 | var b = Vector.fromArray(this.vertices[t[1]]); 1061 | var c = Vector.fromArray(this.vertices[t[2]]); 1062 | var normal = b.subtract(a).cross(c.subtract(a)).unit(); 1063 | this.normals[t[0]] = this.normals[t[0]].add(normal); 1064 | this.normals[t[1]] = this.normals[t[1]].add(normal); 1065 | this.normals[t[2]] = this.normals[t[2]].add(normal); 1066 | } 1067 | for (var i = 0; i < this.vertices.length; i++) { 1068 | this.normals[i] = this.normals[i].unit().toArray(); 1069 | } 1070 | this.compile(); 1071 | return this; 1072 | }, 1073 | 1074 | // ### .computeWireframe() 1075 | // 1076 | // Populate the `lines` index buffer from the `triangles` index buffer. 1077 | computeWireframe: function() { 1078 | var indexer = new Indexer(); 1079 | for (var i = 0; i < this.triangles.length; i++) { 1080 | var t = this.triangles[i]; 1081 | for (var j = 0; j < t.length; j++) { 1082 | var a = t[j], b = t[(j + 1) % t.length]; 1083 | indexer.add([Math.min(a, b), Math.max(a, b)]); 1084 | } 1085 | } 1086 | if (!this.lines) this.addIndexBuffer('lines'); 1087 | this.lines = indexer.unique; 1088 | this.compile(); 1089 | return this; 1090 | }, 1091 | 1092 | // ### .getAABB() 1093 | // 1094 | // Computes the axis-aligned bounding box, which is an object whose `min` and 1095 | // `max` properties contain the minimum and maximum coordinates of all vertices. 1096 | getAABB: function() { 1097 | var aabb = { min: new Vector(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) }; 1098 | aabb.max = aabb.min.negative(); 1099 | for (var i = 0; i < this.vertices.length; i++) { 1100 | var v = Vector.fromArray(this.vertices[i]); 1101 | aabb.min = Vector.min(aabb.min, v); 1102 | aabb.max = Vector.max(aabb.max, v); 1103 | } 1104 | return aabb; 1105 | }, 1106 | 1107 | // ### .getBoundingSphere() 1108 | // 1109 | // Computes a sphere that contains all vertices (not necessarily the smallest 1110 | // sphere). The returned object has two properties, `center` and `radius`. 1111 | getBoundingSphere: function() { 1112 | var aabb = this.getAABB(); 1113 | var sphere = { center: aabb.min.add(aabb.max).divide(2), radius: 0 }; 1114 | for (var i = 0; i < this.vertices.length; i++) { 1115 | sphere.radius = Math.max(sphere.radius, 1116 | Vector.fromArray(this.vertices[i]).subtract(sphere.center).length()); 1117 | } 1118 | return sphere; 1119 | } 1120 | }; 1121 | 1122 | // ### GL.Mesh.plane([options]) 1123 | // 1124 | // Generates a square 2x2 mesh the xy plane centered at the origin. The 1125 | // `options` argument specifies options to pass to the mesh constructor. 1126 | // Additional options include `detailX` and `detailY`, which set the tesselation 1127 | // in x and y, and `detail`, which sets both `detailX` and `detailY` at once. 1128 | // Two triangles are generated by default. 1129 | // Example usage: 1130 | // 1131 | // var mesh1 = GL.Mesh.plane(); 1132 | // var mesh2 = GL.Mesh.plane({ detail: 5 }); 1133 | // var mesh3 = GL.Mesh.plane({ detailX: 20, detailY: 40 }); 1134 | // 1135 | Mesh.plane = function(options) { 1136 | options = options || {}; 1137 | var mesh = new Mesh(options); 1138 | detailX = options.detailX || options.detail || 1; 1139 | detailY = options.detailY || options.detail || 1; 1140 | 1141 | for (var y = 0; y <= detailY; y++) { 1142 | var t = y / detailY; 1143 | for (var x = 0; x <= detailX; x++) { 1144 | var s = x / detailX; 1145 | mesh.vertices.push([2 * s - 1, 2 * t - 1, 0]); 1146 | if (mesh.coords) mesh.coords.push([s, t]); 1147 | if (mesh.normals) mesh.normals.push([0, 0, 1]); 1148 | if (x < detailX && y < detailY) { 1149 | var i = x + y * (detailX + 1); 1150 | mesh.triangles.push([i, i + 1, i + detailX + 1]); 1151 | mesh.triangles.push([i + detailX + 1, i + 1, i + detailX + 2]); 1152 | } 1153 | } 1154 | } 1155 | 1156 | mesh.compile(); 1157 | return mesh; 1158 | }; 1159 | 1160 | var cubeData = [ 1161 | [0, 4, 2, 6, -1, 0, 0], // -x 1162 | [1, 3, 5, 7, +1, 0, 0], // +x 1163 | [0, 1, 4, 5, 0, -1, 0], // -y 1164 | [2, 6, 3, 7, 0, +1, 0], // +y 1165 | [0, 2, 1, 3, 0, 0, -1], // -z 1166 | [4, 5, 6, 7, 0, 0, +1] // +z 1167 | ]; 1168 | 1169 | function pickOctant(i) { 1170 | return new Vector((i & 1) * 2 - 1, (i & 2) - 1, (i & 4) / 2 - 1); 1171 | } 1172 | 1173 | // ### GL.Mesh.cube([options]) 1174 | // 1175 | // Generates a 2x2x2 box centered at the origin. The `options` argument 1176 | // specifies options to pass to the mesh constructor. 1177 | Mesh.cube = function(options) { 1178 | var mesh = new Mesh(options); 1179 | 1180 | for (var i = 0; i < cubeData.length; i++) { 1181 | var data = cubeData[i], v = i * 4; 1182 | for (var j = 0; j < 4; j++) { 1183 | var d = data[j]; 1184 | mesh.vertices.push(pickOctant(d).toArray()); 1185 | if (mesh.coords) mesh.coords.push([j & 1, (j & 2) / 2]); 1186 | if (mesh.normals) mesh.normals.push(data.slice(4, 7)); 1187 | } 1188 | mesh.triangles.push([v, v + 1, v + 2]); 1189 | mesh.triangles.push([v + 2, v + 1, v + 3]); 1190 | } 1191 | 1192 | mesh.compile(); 1193 | return mesh; 1194 | }; 1195 | 1196 | // ### GL.Mesh.sphere([options]) 1197 | // 1198 | // Generates a geodesic sphere of radius 1. The `options` argument specifies 1199 | // options to pass to the mesh constructor in addition to the `detail` option, 1200 | // which controls the tesselation level. The detail is `6` by default. 1201 | // Example usage: 1202 | // 1203 | // var mesh1 = GL.Mesh.sphere(); 1204 | // var mesh2 = GL.Mesh.sphere({ detail: 2 }); 1205 | // 1206 | Mesh.sphere = function(options) { 1207 | function tri(a, b, c) { return flip ? [a, c, b] : [a, b, c]; } 1208 | function fix(x) { return x + (x - x * x) / 2; } 1209 | options = options || {}; 1210 | var mesh = new Mesh(options); 1211 | var indexer = new Indexer(); 1212 | detail = options.detail || 6; 1213 | 1214 | for (var octant = 0; octant < 8; octant++) { 1215 | var scale = pickOctant(octant); 1216 | var flip = scale.x * scale.y * scale.z > 0; 1217 | var data = []; 1218 | for (var i = 0; i <= detail; i++) { 1219 | // Generate a row of vertices on the surface of the sphere 1220 | // using barycentric coordinates. 1221 | for (var j = 0; i + j <= detail; j++) { 1222 | var a = i / detail; 1223 | var b = j / detail; 1224 | var c = (detail - i - j) / detail; 1225 | var vertex = { vertex: new Vector(fix(a), fix(b), fix(c)).unit().multiply(scale).toArray() }; 1226 | if (mesh.coords) vertex.coord = scale.y > 0 ? [1 - a, c] : [c, 1 - a]; 1227 | data.push(indexer.add(vertex)); 1228 | } 1229 | 1230 | // Generate triangles from this row and the previous row. 1231 | if (i > 0) { 1232 | for (var j = 0; i + j <= detail; j++) { 1233 | var a = (i - 1) * (detail + 1) + ((i - 1) - (i - 1) * (i - 1)) / 2 + j; 1234 | var b = i * (detail + 1) + (i - i * i) / 2 + j; 1235 | mesh.triangles.push(tri(data[a], data[a + 1], data[b])); 1236 | if (i + j < detail) { 1237 | mesh.triangles.push(tri(data[b], data[a + 1], data[b + 1])); 1238 | } 1239 | } 1240 | } 1241 | } 1242 | } 1243 | 1244 | // Reconstruct the geometry from the indexer. 1245 | mesh.vertices = indexer.unique.map(function(v) { return v.vertex; }); 1246 | if (mesh.coords) mesh.coords = indexer.unique.map(function(v) { return v.coord; }); 1247 | if (mesh.normals) mesh.normals = mesh.vertices; 1248 | mesh.compile(); 1249 | return mesh; 1250 | }; 1251 | 1252 | // ### GL.Mesh.load(json[, options]) 1253 | // 1254 | // Creates a mesh from the JSON generated by the `convert/convert.py` script. 1255 | // Example usage: 1256 | // 1257 | // var data = { 1258 | // vertices: [[0, 0, 0], [1, 0, 0], [0, 1, 0]], 1259 | // triangles: [[0, 1, 2]] 1260 | // }; 1261 | // var mesh = GL.Mesh.load(data); 1262 | // 1263 | Mesh.load = function(json, options) { 1264 | options = options || {}; 1265 | if (!('coords' in options)) options.coords = !!json.coords; 1266 | if (!('normals' in options)) options.normals = !!json.normals; 1267 | if (!('colors' in options)) options.colors = !!json.colors; 1268 | if (!('triangles' in options)) options.triangles = !!json.triangles; 1269 | if (!('lines' in options)) options.lines = !!json.lines; 1270 | var mesh = new Mesh(options); 1271 | mesh.vertices = json.vertices; 1272 | if (mesh.coords) mesh.coords = json.coords; 1273 | if (mesh.normals) mesh.normals = json.normals; 1274 | if (mesh.colors) mesh.colors = json.colors; 1275 | if (mesh.triangles) mesh.triangles = json.triangles; 1276 | if (mesh.lines) mesh.lines = json.lines; 1277 | mesh.compile(); 1278 | return mesh; 1279 | }; 1280 | 1281 | // src/raytracer.js 1282 | // Provides a convenient raytracing interface. 1283 | 1284 | // ### new GL.HitTest([t, hit, normal]) 1285 | // 1286 | // This is the object used to return hit test results. If there are no 1287 | // arguments, the constructed argument represents a hit infinitely far 1288 | // away. 1289 | function HitTest(t, hit, normal) { 1290 | this.t = arguments.length ? t : Number.MAX_VALUE; 1291 | this.hit = hit; 1292 | this.normal = normal; 1293 | } 1294 | 1295 | // ### .mergeWith(other) 1296 | // 1297 | // Changes this object to be the closer of the two hit test results. 1298 | HitTest.prototype = { 1299 | mergeWith: function(other) { 1300 | if (other.t > 0 && other.t < this.t) { 1301 | this.t = other.t; 1302 | this.hit = other.hit; 1303 | this.normal = other.normal; 1304 | } 1305 | } 1306 | }; 1307 | 1308 | // ### new GL.Raytracer() 1309 | // 1310 | // This will read the current modelview matrix, projection matrix, and viewport, 1311 | // reconstruct the eye position, and store enough information to later generate 1312 | // per-pixel rays using `getRayForPixel()`. 1313 | // 1314 | // Example usage: 1315 | // 1316 | // var tracer = new GL.Raytracer(); 1317 | // var ray = tracer.getRayForPixel( 1318 | // gl.canvas.width / 2, 1319 | // gl.canvas.height / 2); 1320 | // var result = GL.Raytracer.hitTestSphere( 1321 | // tracer.eye, ray, new GL.Vector(0, 0, 0), 1); 1322 | function Raytracer() { 1323 | var v = gl.getParameter(gl.VIEWPORT); 1324 | var m = gl.modelviewMatrix.m; 1325 | 1326 | var axisX = new Vector(m[0], m[4], m[8]); 1327 | var axisY = new Vector(m[1], m[5], m[9]); 1328 | var axisZ = new Vector(m[2], m[6], m[10]); 1329 | var offset = new Vector(m[3], m[7], m[11]); 1330 | this.eye = new Vector(-offset.dot(axisX), -offset.dot(axisY), -offset.dot(axisZ)); 1331 | 1332 | var minX = v[0], maxX = minX + v[2]; 1333 | var minY = v[1], maxY = minY + v[3]; 1334 | this.ray00 = gl.unProject(minX, minY, 1).subtract(this.eye); 1335 | this.ray10 = gl.unProject(maxX, minY, 1).subtract(this.eye); 1336 | this.ray01 = gl.unProject(minX, maxY, 1).subtract(this.eye); 1337 | this.ray11 = gl.unProject(maxX, maxY, 1).subtract(this.eye); 1338 | this.viewport = v; 1339 | } 1340 | 1341 | Raytracer.prototype = { 1342 | // ### .getRayForPixel(x, y) 1343 | // 1344 | // Returns the ray originating from the camera and traveling through the pixel `x, y`. 1345 | getRayForPixel: function(x, y) { 1346 | x = (x - this.viewport[0]) / this.viewport[2]; 1347 | y = 1 - (y - this.viewport[1]) / this.viewport[3]; 1348 | var ray0 = Vector.lerp(this.ray00, this.ray10, x); 1349 | var ray1 = Vector.lerp(this.ray01, this.ray11, x); 1350 | return Vector.lerp(ray0, ray1, y).unit(); 1351 | } 1352 | }; 1353 | 1354 | // ### GL.Raytracer.hitTestBox(origin, ray, min, max) 1355 | // 1356 | // Traces the ray starting from `origin` along `ray` against the axis-aligned box 1357 | // whose coordinates extend from `min` to `max`. Returns a `HitTest` with the 1358 | // information or `null` for no intersection. 1359 | // 1360 | // This implementation uses the [slab intersection method](http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter3.htm). 1361 | Raytracer.hitTestBox = function(origin, ray, min, max) { 1362 | var tMin = min.subtract(origin).divide(ray); 1363 | var tMax = max.subtract(origin).divide(ray); 1364 | var t1 = Vector.min(tMin, tMax); 1365 | var t2 = Vector.max(tMin, tMax); 1366 | var tNear = t1.max(); 1367 | var tFar = t2.min(); 1368 | 1369 | if (tNear > 0 && tNear < tFar) { 1370 | var epsilon = 1.0e-6, hit = origin.add(ray.multiply(tNear)); 1371 | min = min.add(epsilon); 1372 | max = max.subtract(epsilon); 1373 | return new HitTest(tNear, hit, new Vector( 1374 | (hit.x > max.x) - (hit.x < min.x), 1375 | (hit.y > max.y) - (hit.y < min.y), 1376 | (hit.z > max.z) - (hit.z < min.z) 1377 | )); 1378 | } 1379 | 1380 | return null; 1381 | }; 1382 | 1383 | // ### GL.Raytracer.hitTestSphere(origin, ray, center, radius) 1384 | // 1385 | // Traces the ray starting from `origin` along `ray` against the sphere defined 1386 | // by `center` and `radius`. Returns a `HitTest` with the information or `null` 1387 | // for no intersection. 1388 | Raytracer.hitTestSphere = function(origin, ray, center, radius) { 1389 | var offset = origin.subtract(center); 1390 | var a = ray.dot(ray); 1391 | var b = 2 * ray.dot(offset); 1392 | var c = offset.dot(offset) - radius * radius; 1393 | var discriminant = b * b - 4 * a * c; 1394 | 1395 | if (discriminant > 0) { 1396 | var t = (-b - Math.sqrt(discriminant)) / (2 * a), hit = origin.add(ray.multiply(t)); 1397 | return new HitTest(t, hit, hit.subtract(center).divide(radius)); 1398 | } 1399 | 1400 | return null; 1401 | }; 1402 | 1403 | // ### GL.Raytracer.hitTestTriangle(origin, ray, a, b, c) 1404 | // 1405 | // Traces the ray starting from `origin` along `ray` against the triangle defined 1406 | // by the points `a`, `b`, and `c`. Returns a `HitTest` with the information or 1407 | // `null` for no intersection. 1408 | Raytracer.hitTestTriangle = function(origin, ray, a, b, c) { 1409 | var ab = b.subtract(a); 1410 | var ac = c.subtract(a); 1411 | var normal = ab.cross(ac).unit(); 1412 | var t = normal.dot(a.subtract(origin)) / normal.dot(ray); 1413 | 1414 | if (t > 0) { 1415 | var hit = origin.add(ray.multiply(t)); 1416 | var toHit = hit.subtract(a); 1417 | var dot00 = ac.dot(ac); 1418 | var dot01 = ac.dot(ab); 1419 | var dot02 = ac.dot(toHit); 1420 | var dot11 = ab.dot(ab); 1421 | var dot12 = ab.dot(toHit); 1422 | var divide = dot00 * dot11 - dot01 * dot01; 1423 | var u = (dot11 * dot02 - dot01 * dot12) / divide; 1424 | var v = (dot00 * dot12 - dot01 * dot02) / divide; 1425 | if (u >= 0 && v >= 0 && u + v <= 1) return new HitTest(t, hit, normal); 1426 | } 1427 | 1428 | return null; 1429 | }; 1430 | 1431 | // src/shader.js 1432 | // Provides a convenient wrapper for WebGL shaders. A few uniforms and attributes, 1433 | // prefixed with `gl_`, are automatically added to all shader sources to make 1434 | // simple shaders easier to write. 1435 | // 1436 | // Example usage: 1437 | // 1438 | // var shader = new GL.Shader('\ 1439 | // void main() {\ 1440 | // gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\ 1441 | // }\ 1442 | // ', '\ 1443 | // uniform vec4 color;\ 1444 | // void main() {\ 1445 | // gl_FragColor = color;\ 1446 | // }\ 1447 | // '); 1448 | // 1449 | // shader.uniforms({ 1450 | // color: [1, 0, 0, 1] 1451 | // }).draw(mesh); 1452 | 1453 | function regexMap(regex, text, callback) { 1454 | while ((result = regex.exec(text)) != null) { 1455 | callback(result); 1456 | } 1457 | } 1458 | 1459 | // Non-standard names beginning with `gl_` must be mangled because they will 1460 | // otherwise cause a compiler error. 1461 | var LIGHTGL_PREFIX = 'LIGHTGL'; 1462 | 1463 | // ### new GL.Shader(vertexSource, fragmentSource) 1464 | // 1465 | // Compiles a shader program using the provided vertex and fragment shaders. 1466 | function Shader(vertexSource, fragmentSource) { 1467 | // Allow passing in the id of an HTML script tag with the source 1468 | function followScriptTagById(id) { 1469 | var element = document.getElementById(id); 1470 | return element ? element.text : id; 1471 | } 1472 | vertexSource = followScriptTagById(vertexSource); 1473 | fragmentSource = followScriptTagById(fragmentSource); 1474 | 1475 | // Headers are prepended to the sources to provide some automatic functionality. 1476 | var header = '\ 1477 | uniform mat3 gl_NormalMatrix;\ 1478 | uniform mat4 gl_ModelViewMatrix;\ 1479 | uniform mat4 gl_ProjectionMatrix;\ 1480 | uniform mat4 gl_ModelViewProjectionMatrix;\ 1481 | uniform mat4 gl_ModelViewMatrixInverse;\ 1482 | uniform mat4 gl_ProjectionMatrixInverse;\ 1483 | uniform mat4 gl_ModelViewProjectionMatrixInverse;\ 1484 | '; 1485 | var vertexHeader = header + '\ 1486 | attribute vec4 gl_Vertex;\ 1487 | attribute vec4 gl_TexCoord;\ 1488 | attribute vec3 gl_Normal;\ 1489 | attribute vec4 gl_Color;\ 1490 | vec4 ftransform() {\ 1491 | return gl_ModelViewProjectionMatrix * gl_Vertex;\ 1492 | }\ 1493 | '; 1494 | var fragmentHeader = '\ 1495 | precision highp float;\ 1496 | ' + header; 1497 | 1498 | // Check for the use of built-in matrices that require expensive matrix 1499 | // multiplications to compute, and record these in `usedMatrices`. 1500 | var source = vertexSource + fragmentSource; 1501 | var usedMatrices = {}; 1502 | regexMap(/\b(gl_[^;]*)\b;/g, header, function(groups) { 1503 | var name = groups[1]; 1504 | if (source.indexOf(name) != -1) { 1505 | var capitalLetters = name.replace(/[a-z_]/g, ''); 1506 | usedMatrices[capitalLetters] = LIGHTGL_PREFIX + name; 1507 | } 1508 | }); 1509 | if (source.indexOf('ftransform') != -1) usedMatrices.MVPM = LIGHTGL_PREFIX + 'gl_ModelViewProjectionMatrix'; 1510 | this.usedMatrices = usedMatrices; 1511 | 1512 | // The `gl_` prefix must be substituted for something else to avoid compile 1513 | // errors, since it's a reserved prefix. This prefixes all reserved names with 1514 | // `_`. The header is inserted after any extensions, since those must come 1515 | // first. 1516 | function fix(header, source) { 1517 | var replaced = {}; 1518 | var match = /^((\s*\/\/.*\n|\s*#extension.*\n)+)[^]*$/.exec(source); 1519 | source = match ? match[1] + header + source.substr(match[1].length) : header + source; 1520 | regexMap(/\bgl_\w+\b/g, header, function(result) { 1521 | if (!(result in replaced)) { 1522 | source = source.replace(new RegExp('\\b' + result + '\\b', 'g'), LIGHTGL_PREFIX + result); 1523 | replaced[result] = true; 1524 | } 1525 | }); 1526 | return source; 1527 | } 1528 | vertexSource = fix(vertexHeader, vertexSource); 1529 | fragmentSource = fix(fragmentHeader, fragmentSource); 1530 | 1531 | // Compile and link errors are thrown as strings. 1532 | function compileSource(type, source) { 1533 | var shader = gl.createShader(type); 1534 | gl.shaderSource(shader, source); 1535 | gl.compileShader(shader); 1536 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 1537 | throw new Error('compile error: ' + gl.getShaderInfoLog(shader)); 1538 | } 1539 | return shader; 1540 | } 1541 | this.program = gl.createProgram(); 1542 | gl.attachShader(this.program, compileSource(gl.VERTEX_SHADER, vertexSource)); 1543 | gl.attachShader(this.program, compileSource(gl.FRAGMENT_SHADER, fragmentSource)); 1544 | gl.linkProgram(this.program); 1545 | if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 1546 | throw new Error('link error: ' + gl.getProgramInfoLog(this.program)); 1547 | } 1548 | this.attributes = {}; 1549 | this.uniformLocations = {}; 1550 | 1551 | // Sampler uniforms need to be uploaded using `gl.uniform1i()` instead of `gl.uniform1f()`. 1552 | // To do this automatically, we detect and remember all uniform samplers in the source code. 1553 | var isSampler = {}; 1554 | regexMap(/uniform\s+sampler(1D|2D|3D|Cube)\s+(\w+)\s*;/g, vertexSource + fragmentSource, function(groups) { 1555 | isSampler[groups[2]] = 1; 1556 | }); 1557 | this.isSampler = isSampler; 1558 | } 1559 | 1560 | function isArray(obj) { 1561 | var str = Object.prototype.toString.call(obj); 1562 | return str == '[object Array]' || str == '[object Float32Array]'; 1563 | } 1564 | 1565 | function isNumber(obj) { 1566 | var str = Object.prototype.toString.call(obj); 1567 | return str == '[object Number]' || str == '[object Boolean]'; 1568 | } 1569 | 1570 | var tempMatrix = new Matrix(); 1571 | var resultMatrix = new Matrix(); 1572 | 1573 | Shader.prototype = { 1574 | // ### .uniforms(uniforms) 1575 | // 1576 | // Set a uniform for each property of `uniforms`. The correct `gl.uniform*()` method is 1577 | // inferred from the value types and from the stored uniform sampler flags. 1578 | uniforms: function(uniforms) { 1579 | gl.useProgram(this.program); 1580 | 1581 | for (var name in uniforms) { 1582 | var location = this.uniformLocations[name] || gl.getUniformLocation(this.program, name); 1583 | if (!location) continue; 1584 | this.uniformLocations[name] = location; 1585 | var value = uniforms[name]; 1586 | if (value instanceof Vector) { 1587 | value = [value.x, value.y, value.z]; 1588 | } else if (value instanceof Matrix) { 1589 | value = value.m; 1590 | } 1591 | if (isArray(value)) { 1592 | switch (value.length) { 1593 | case 1: gl.uniform1fv(location, new Float32Array(value)); break; 1594 | case 2: gl.uniform2fv(location, new Float32Array(value)); break; 1595 | case 3: gl.uniform3fv(location, new Float32Array(value)); break; 1596 | case 4: gl.uniform4fv(location, new Float32Array(value)); break; 1597 | // Matrices are automatically transposed, since WebGL uses column-major 1598 | // indices instead of row-major indices. 1599 | case 9: gl.uniformMatrix3fv(location, false, new Float32Array([ 1600 | value[0], value[3], value[6], 1601 | value[1], value[4], value[7], 1602 | value[2], value[5], value[8] 1603 | ])); break; 1604 | case 16: gl.uniformMatrix4fv(location, false, new Float32Array([ 1605 | value[0], value[4], value[8], value[12], 1606 | value[1], value[5], value[9], value[13], 1607 | value[2], value[6], value[10], value[14], 1608 | value[3], value[7], value[11], value[15] 1609 | ])); break; 1610 | default: throw new Error('don\'t know how to load uniform "' + name + '" of length ' + value.length); 1611 | } 1612 | } else if (isNumber(value)) { 1613 | (this.isSampler[name] ? gl.uniform1i : gl.uniform1f).call(gl, location, value); 1614 | } else { 1615 | throw new Error('attempted to set uniform "' + name + '" to invalid value ' + value); 1616 | } 1617 | } 1618 | 1619 | return this; 1620 | }, 1621 | 1622 | // ### .draw(mesh[, mode]) 1623 | // 1624 | // Sets all uniform matrix attributes, binds all relevant buffers, and draws the 1625 | // mesh geometry as indexed triangles or indexed lines. Set `mode` to `gl.LINES` 1626 | // (and either add indices to `lines` or call `computeWireframe()`) to draw the 1627 | // mesh in wireframe. 1628 | draw: function(mesh, mode) { 1629 | this.drawBuffers(mesh.vertexBuffers, 1630 | mesh.indexBuffers[mode == gl.LINES ? 'lines' : 'triangles'], 1631 | arguments.length < 2 ? gl.TRIANGLES : mode); 1632 | }, 1633 | 1634 | // ### .drawBuffers(vertexBuffers, indexBuffer, mode) 1635 | // 1636 | // Sets all uniform matrix attributes, binds all relevant buffers, and draws the 1637 | // indexed mesh geometry. The `vertexBuffers` argument is a map from attribute 1638 | // names to `Buffer` objects of type `gl.ARRAY_BUFFER`, `indexBuffer` is a `Buffer` 1639 | // object of type `gl.ELEMENT_ARRAY_BUFFER`, and `mode` is a WebGL primitive mode 1640 | // like `gl.TRIANGLES` or `gl.LINES`. This method automatically creates and caches 1641 | // vertex attribute pointers for attributes as needed. 1642 | drawBuffers: function(vertexBuffers, indexBuffer, mode) { 1643 | // Only construct up the built-in matrices we need for this shader. 1644 | var used = this.usedMatrices; 1645 | var MVM = gl.modelviewMatrix; 1646 | var PM = gl.projectionMatrix; 1647 | var MVMI = (used.MVMI || used.NM) ? MVM.inverse() : null; 1648 | var PMI = (used.PMI) ? PM.inverse() : null; 1649 | var MVPM = (used.MVPM || used.MVPMI) ? PM.multiply(MVM) : null; 1650 | var matrices = {}; 1651 | if (used.MVM) matrices[used.MVM] = MVM; 1652 | if (used.MVMI) matrices[used.MVMI] = MVMI; 1653 | if (used.PM) matrices[used.PM] = PM; 1654 | if (used.PMI) matrices[used.PMI] = PMI; 1655 | if (used.MVPM) matrices[used.MVPM] = MVPM; 1656 | if (used.MVPMI) matrices[used.MVPMI] = MVPM.inverse(); 1657 | if (used.NM) { 1658 | var m = MVMI.m; 1659 | matrices[used.NM] = [m[0], m[4], m[8], m[1], m[5], m[9], m[2], m[6], m[10]]; 1660 | } 1661 | this.uniforms(matrices); 1662 | 1663 | // Create and enable attribute pointers as necessary. 1664 | var length = 0; 1665 | for (var attribute in vertexBuffers) { 1666 | var buffer = vertexBuffers[attribute]; 1667 | var location = this.attributes[attribute] || 1668 | gl.getAttribLocation(this.program, attribute.replace(/^(gl_.*)$/, LIGHTGL_PREFIX + '$1')); 1669 | if (location == -1 || !buffer.buffer) continue; 1670 | this.attributes[attribute] = location; 1671 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buffer); 1672 | gl.enableVertexAttribArray(location); 1673 | gl.vertexAttribPointer(location, buffer.buffer.spacing, gl.FLOAT, false, 0, 0); 1674 | length = buffer.buffer.length / buffer.buffer.spacing; 1675 | } 1676 | 1677 | // Disable unused attribute pointers. 1678 | for (var attribute in this.attributes) { 1679 | if (!(attribute in vertexBuffers)) { 1680 | gl.disableVertexAttribArray(this.attributes[attribute]); 1681 | } 1682 | } 1683 | 1684 | // Draw the geometry. 1685 | if (length && (!indexBuffer || indexBuffer.buffer)) { 1686 | if (indexBuffer) { 1687 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer.buffer); 1688 | gl.drawElements(mode, indexBuffer.buffer.length, gl.UNSIGNED_SHORT, 0); 1689 | } else { 1690 | gl.drawArrays(mode, 0, length); 1691 | } 1692 | } 1693 | 1694 | return this; 1695 | } 1696 | }; 1697 | 1698 | // src/texture.js 1699 | // Provides a simple wrapper around WebGL textures that supports render-to-texture. 1700 | 1701 | // ### new GL.Texture(width, height[, options]) 1702 | // 1703 | // The arguments `width` and `height` give the size of the texture in texels. 1704 | // WebGL texture dimensions must be powers of two unless `filter` is set to 1705 | // either `gl.NEAREST` or `gl.LINEAR` and `wrap` is set to `gl.CLAMP_TO_EDGE` 1706 | // (which they are by default). 1707 | // 1708 | // Texture parameters can be passed in via the `options` argument. 1709 | // Example usage: 1710 | // 1711 | // var t = new GL.Texture(256, 256, { 1712 | // // Defaults to gl.LINEAR, set both at once with "filter" 1713 | // magFilter: gl.NEAREST, 1714 | // minFilter: gl.LINEAR, 1715 | // 1716 | // // Defaults to gl.CLAMP_TO_EDGE, set both at once with "wrap" 1717 | // wrapS: gl.REPEAT, 1718 | // wrapT: gl.REPEAT, 1719 | // 1720 | // format: gl.RGB, // Defaults to gl.RGBA 1721 | // type: gl.FLOAT // Defaults to gl.UNSIGNED_BYTE 1722 | // }); 1723 | function Texture(width, height, options) { 1724 | options = options || {}; 1725 | this.id = gl.createTexture(); 1726 | this.width = width; 1727 | this.height = height; 1728 | this.format = options.format || gl.RGBA; 1729 | this.type = options.type || gl.UNSIGNED_BYTE; 1730 | var magFilter = options.filter || options.magFilter || gl.LINEAR; 1731 | var minFilter = options.filter || options.minFilter || gl.LINEAR; 1732 | if (this.type === gl.FLOAT) { 1733 | if (!Texture.canUseFloatingPointTextures()) { 1734 | throw new Error('OES_texture_float is required but not supported'); 1735 | } 1736 | if ((minFilter !== gl.NEAREST || magFilter !== gl.NEAREST) && 1737 | !Texture.canUseFloatingPointLinearFiltering()) { 1738 | throw new Error('OES_texture_float_linear is required but not supported'); 1739 | } 1740 | } else if (this.type === gl.HALF_FLOAT_OES) { 1741 | if (!Texture.canUseHalfFloatingPointTextures()) { 1742 | throw new Error('OES_texture_half_float is required but not supported'); 1743 | } 1744 | if ((minFilter !== gl.NEAREST || magFilter !== gl.NEAREST) && 1745 | !Texture.canUseHalfFloatingPointLinearFiltering()) { 1746 | throw new Error('OES_texture_half_float_linear is required but not supported'); 1747 | } 1748 | } 1749 | gl.bindTexture(gl.TEXTURE_2D, this.id); 1750 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 1751 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter); 1752 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter); 1753 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.wrap || options.wrapS || gl.CLAMP_TO_EDGE); 1754 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, options.wrap || options.wrapT || gl.CLAMP_TO_EDGE); 1755 | gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, this.type, null); 1756 | } 1757 | 1758 | var framebuffer; 1759 | var renderbuffer; 1760 | var checkerboardCanvas; 1761 | 1762 | Texture.prototype = { 1763 | // ### .bind([unit]) 1764 | // 1765 | // Bind this texture to the given texture unit (0-7, defaults to 0). 1766 | bind: function(unit) { 1767 | gl.activeTexture(gl.TEXTURE0 + (unit || 0)); 1768 | gl.bindTexture(gl.TEXTURE_2D, this.id); 1769 | }, 1770 | 1771 | // ### .unbind([unit]) 1772 | // 1773 | // Clear the given texture unit (0-7, defaults to 0). 1774 | unbind: function(unit) { 1775 | gl.activeTexture(gl.TEXTURE0 + (unit || 0)); 1776 | gl.bindTexture(gl.TEXTURE_2D, null); 1777 | }, 1778 | 1779 | // ### .canDrawTo() 1780 | // 1781 | // Check if rendering to this texture is supported. It may not be supported 1782 | // for floating-point textures on some configurations. 1783 | canDrawTo: function() { 1784 | framebuffer = framebuffer || gl.createFramebuffer(); 1785 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 1786 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); 1787 | var result = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; 1788 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1789 | return result; 1790 | }, 1791 | 1792 | // ### .drawTo(callback) 1793 | // 1794 | // Render all draw calls in `callback` to this texture. This method sets up 1795 | // a framebuffer with this texture as the color attachment and a renderbuffer 1796 | // as the depth attachment. It also temporarily changes the viewport to the 1797 | // size of the texture. 1798 | // 1799 | // Example usage: 1800 | // 1801 | // texture.drawTo(function() { 1802 | // gl.clearColor(1, 0, 0, 1); 1803 | // gl.clear(gl.COLOR_BUFFER_BIT); 1804 | // }); 1805 | drawTo: function(callback) { 1806 | var v = gl.getParameter(gl.VIEWPORT); 1807 | framebuffer = framebuffer || gl.createFramebuffer(); 1808 | renderbuffer = renderbuffer || gl.createRenderbuffer(); 1809 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 1810 | gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); 1811 | if (this.width != renderbuffer.width || this.height != renderbuffer.height) { 1812 | renderbuffer.width = this.width; 1813 | renderbuffer.height = this.height; 1814 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); 1815 | } 1816 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); 1817 | gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); 1818 | if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { 1819 | throw new Error('Rendering to this texture is not supported (incomplete framebuffer)'); 1820 | } 1821 | gl.viewport(0, 0, this.width, this.height); 1822 | 1823 | callback(); 1824 | 1825 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 1826 | gl.bindRenderbuffer(gl.RENDERBUFFER, null); 1827 | gl.viewport(v[0], v[1], v[2], v[3]); 1828 | }, 1829 | 1830 | // ### .swapWith(other) 1831 | // 1832 | // Switch this texture with `other`, useful for the ping-pong rendering 1833 | // technique used in multi-stage rendering. 1834 | swapWith: function(other) { 1835 | var temp; 1836 | temp = other.id; other.id = this.id; this.id = temp; 1837 | temp = other.width; other.width = this.width; this.width = temp; 1838 | temp = other.height; other.height = this.height; this.height = temp; 1839 | } 1840 | }; 1841 | 1842 | // ### GL.Texture.fromImage(image[, options]) 1843 | // 1844 | // Return a new image created from `image`, an `` tag. 1845 | Texture.fromImage = function(image, options) { 1846 | options = options || {}; 1847 | var texture = new Texture(image.width, image.height, options); 1848 | try { 1849 | gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, texture.type, image); 1850 | } catch (e) { 1851 | if (location.protocol == 'file:') { 1852 | throw new Error('image not loaded for security reasons (serve this page over "http://" instead)'); 1853 | } else { 1854 | throw new Error('image not loaded for security reasons (image must originate from the same ' + 1855 | 'domain as this page or use Cross-Origin Resource Sharing)'); 1856 | } 1857 | } 1858 | if (options.minFilter && options.minFilter != gl.NEAREST && options.minFilter != gl.LINEAR) { 1859 | gl.generateMipmap(gl.TEXTURE_2D); 1860 | } 1861 | return texture; 1862 | }; 1863 | 1864 | // ### GL.Texture.fromURL(url[, options]) 1865 | // 1866 | // Returns a checkerboard texture that will switch to the correct texture when 1867 | // it loads. 1868 | Texture.fromURL = function(url, options) { 1869 | checkerboardCanvas = checkerboardCanvas || (function() { 1870 | var c = document.createElement('canvas').getContext('2d'); 1871 | c.canvas.width = c.canvas.height = 128; 1872 | for (var y = 0; y < c.canvas.height; y += 16) { 1873 | for (var x = 0; x < c.canvas.width; x += 16) { 1874 | c.fillStyle = (x ^ y) & 16 ? '#FFF' : '#DDD'; 1875 | c.fillRect(x, y, 16, 16); 1876 | } 1877 | } 1878 | return c.canvas; 1879 | })(); 1880 | var texture = Texture.fromImage(checkerboardCanvas, options); 1881 | var image = new Image(); 1882 | var context = gl; 1883 | image.onload = function() { 1884 | context.makeCurrent(); 1885 | Texture.fromImage(image, options).swapWith(texture); 1886 | }; 1887 | image.src = url; 1888 | return texture; 1889 | }; 1890 | 1891 | // ### GL.Texture.canUseFloatingPointTextures() 1892 | // 1893 | // Returns false if `gl.FLOAT` is not supported as a texture type. This is the 1894 | // `OES_texture_float` extension. 1895 | Texture.canUseFloatingPointTextures = function() { 1896 | return !!gl.getExtension('OES_texture_float'); 1897 | }; 1898 | 1899 | // ### GL.Texture.canUseFloatingPointLinearFiltering() 1900 | // 1901 | // Returns false if `gl.LINEAR` is not supported as a texture filter mode for 1902 | // textures of type `gl.FLOAT`. This is the `OES_texture_float_linear` 1903 | // extension. 1904 | Texture.canUseFloatingPointLinearFiltering = function() { 1905 | return !!gl.getExtension('OES_texture_float_linear'); 1906 | }; 1907 | 1908 | // ### GL.Texture.canUseFloatingPointTextures() 1909 | // 1910 | // Returns false if `gl.HALF_FLOAT_OES` is not supported as a texture type. 1911 | // This is the `OES_texture_half_float` extension. 1912 | Texture.canUseHalfFloatingPointTextures = function() { 1913 | return !!gl.getExtension('OES_texture_half_float'); 1914 | }; 1915 | 1916 | // ### GL.Texture.canUseFloatingPointLinearFiltering() 1917 | // 1918 | // Returns false if `gl.LINEAR` is not supported as a texture filter mode for 1919 | // textures of type `gl.HALF_FLOAT_OES`. This is the 1920 | // `OES_texture_half_float_linear` extension. 1921 | Texture.canUseHalfFloatingPointLinearFiltering = function() { 1922 | return !!gl.getExtension('OES_texture_half_float_linear'); 1923 | }; 1924 | 1925 | // src/vector.js 1926 | // Provides a simple 3D vector class. Vector operations can be done using member 1927 | // functions, which return new vectors, or static functions, which reuse 1928 | // existing vectors to avoid generating garbage. 1929 | function Vector(x, y, z) { 1930 | this.x = x || 0; 1931 | this.y = y || 0; 1932 | this.z = z || 0; 1933 | } 1934 | 1935 | // ### Instance Methods 1936 | // The methods `add()`, `subtract()`, `multiply()`, and `divide()` can all 1937 | // take either a vector or a number as an argument. 1938 | Vector.prototype = { 1939 | negative: function() { 1940 | return new Vector(-this.x, -this.y, -this.z); 1941 | }, 1942 | add: function(v) { 1943 | if (v instanceof Vector) return new Vector(this.x + v.x, this.y + v.y, this.z + v.z); 1944 | else return new Vector(this.x + v, this.y + v, this.z + v); 1945 | }, 1946 | subtract: function(v) { 1947 | if (v instanceof Vector) return new Vector(this.x - v.x, this.y - v.y, this.z - v.z); 1948 | else return new Vector(this.x - v, this.y - v, this.z - v); 1949 | }, 1950 | multiply: function(v) { 1951 | if (v instanceof Vector) return new Vector(this.x * v.x, this.y * v.y, this.z * v.z); 1952 | else return new Vector(this.x * v, this.y * v, this.z * v); 1953 | }, 1954 | divide: function(v) { 1955 | if (v instanceof Vector) return new Vector(this.x / v.x, this.y / v.y, this.z / v.z); 1956 | else return new Vector(this.x / v, this.y / v, this.z / v); 1957 | }, 1958 | equals: function(v) { 1959 | return this.x == v.x && this.y == v.y && this.z == v.z; 1960 | }, 1961 | dot: function(v) { 1962 | return this.x * v.x + this.y * v.y + this.z * v.z; 1963 | }, 1964 | cross: function(v) { 1965 | return new Vector( 1966 | this.y * v.z - this.z * v.y, 1967 | this.z * v.x - this.x * v.z, 1968 | this.x * v.y - this.y * v.x 1969 | ); 1970 | }, 1971 | length: function() { 1972 | return Math.sqrt(this.dot(this)); 1973 | }, 1974 | unit: function() { 1975 | return this.divide(this.length()); 1976 | }, 1977 | min: function() { 1978 | return Math.min(Math.min(this.x, this.y), this.z); 1979 | }, 1980 | max: function() { 1981 | return Math.max(Math.max(this.x, this.y), this.z); 1982 | }, 1983 | toAngles: function() { 1984 | return { 1985 | theta: Math.atan2(this.z, this.x), 1986 | phi: Math.asin(this.y / this.length()) 1987 | }; 1988 | }, 1989 | angleTo: function(a) { 1990 | return Math.acos(this.dot(a) / (this.length() * a.length())); 1991 | }, 1992 | toArray: function(n) { 1993 | return [this.x, this.y, this.z].slice(0, n || 3); 1994 | }, 1995 | clone: function() { 1996 | return new Vector(this.x, this.y, this.z); 1997 | }, 1998 | init: function(x, y, z) { 1999 | this.x = x; this.y = y; this.z = z; 2000 | return this; 2001 | } 2002 | }; 2003 | 2004 | // ### Static Methods 2005 | // `Vector.randomDirection()` returns a vector with a length of 1 and a 2006 | // statistically uniform direction. `Vector.lerp()` performs linear 2007 | // interpolation between two vectors. 2008 | Vector.negative = function(a, b) { 2009 | b.x = -a.x; b.y = -a.y; b.z = -a.z; 2010 | return b; 2011 | }; 2012 | Vector.add = function(a, b, c) { 2013 | if (b instanceof Vector) { c.x = a.x + b.x; c.y = a.y + b.y; c.z = a.z + b.z; } 2014 | else { c.x = a.x + b; c.y = a.y + b; c.z = a.z + b; } 2015 | return c; 2016 | }; 2017 | Vector.subtract = function(a, b, c) { 2018 | if (b instanceof Vector) { c.x = a.x - b.x; c.y = a.y - b.y; c.z = a.z - b.z; } 2019 | else { c.x = a.x - b; c.y = a.y - b; c.z = a.z - b; } 2020 | return c; 2021 | }; 2022 | Vector.multiply = function(a, b, c) { 2023 | if (b instanceof Vector) { c.x = a.x * b.x; c.y = a.y * b.y; c.z = a.z * b.z; } 2024 | else { c.x = a.x * b; c.y = a.y * b; c.z = a.z * b; } 2025 | return c; 2026 | }; 2027 | Vector.divide = function(a, b, c) { 2028 | if (b instanceof Vector) { c.x = a.x / b.x; c.y = a.y / b.y; c.z = a.z / b.z; } 2029 | else { c.x = a.x / b; c.y = a.y / b; c.z = a.z / b; } 2030 | return c; 2031 | }; 2032 | Vector.cross = function(a, b, c) { 2033 | c.x = a.y * b.z - a.z * b.y; 2034 | c.y = a.z * b.x - a.x * b.z; 2035 | c.z = a.x * b.y - a.y * b.x; 2036 | return c; 2037 | }; 2038 | Vector.unit = function(a, b) { 2039 | var length = a.length(); 2040 | b.x = a.x / length; 2041 | b.y = a.y / length; 2042 | b.z = a.z / length; 2043 | return b; 2044 | }; 2045 | Vector.fromAngles = function(theta, phi) { 2046 | return new Vector(Math.cos(theta) * Math.cos(phi), Math.sin(phi), Math.sin(theta) * Math.cos(phi)); 2047 | }; 2048 | Vector.randomDirection = function() { 2049 | return Vector.fromAngles(Math.random() * Math.PI * 2, Math.asin(Math.random() * 2 - 1)); 2050 | }; 2051 | Vector.min = function(a, b) { 2052 | return new Vector(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.min(a.z, b.z)); 2053 | }; 2054 | Vector.max = function(a, b) { 2055 | return new Vector(Math.max(a.x, b.x), Math.max(a.y, b.y), Math.max(a.z, b.z)); 2056 | }; 2057 | Vector.lerp = function(a, b, fraction) { 2058 | return b.subtract(a).multiply(fraction).add(a); 2059 | }; 2060 | Vector.fromArray = function(a) { 2061 | return new Vector(a[0], a[1], a[2]); 2062 | }; 2063 | Vector.angleBetween = function(a, b) { 2064 | return a.angleTo(b); 2065 | }; 2066 | 2067 | return GL; 2068 | })(); 2069 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WebGL Water 3 | * http://madebyevan.com/webgl-water/ 4 | * 5 | * Copyright 2011 Evan Wallace 6 | * Released under the MIT license 7 | */ 8 | 9 | function text2html(text) { 10 | return text.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
'); 11 | } 12 | 13 | function handleError(text) { 14 | var html = text2html(text); 15 | if (html == 'WebGL not supported') { 16 | html = 'Your browser does not support WebGL.
Please see\ 17 | \ 18 | Getting a WebGL Implementation.'; 19 | } 20 | var loading = document.getElementById('loading'); 21 | loading.innerHTML = html; 22 | loading.style.zIndex = 1; 23 | } 24 | 25 | window.onerror = handleError; 26 | 27 | var gl = GL.create(); 28 | var water; 29 | var cubemap; 30 | var renderer; 31 | var angleX = -25; 32 | var angleY = -200.5; 33 | 34 | // Sphere physics info 35 | var useSpherePhysics = false; 36 | var center; 37 | var oldCenter; 38 | var velocity; 39 | var gravity; 40 | var radius; 41 | var paused = false; 42 | 43 | window.onload = function() { 44 | var ratio = window.devicePixelRatio || 1; 45 | var help = document.getElementById('help'); 46 | 47 | function onresize() { 48 | var width = innerWidth - help.clientWidth - 20; 49 | var height = innerHeight; 50 | gl.canvas.width = width * ratio; 51 | gl.canvas.height = height * ratio; 52 | gl.canvas.style.width = width + 'px'; 53 | gl.canvas.style.height = height + 'px'; 54 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 55 | gl.matrixMode(gl.PROJECTION); 56 | gl.loadIdentity(); 57 | gl.perspective(45, gl.canvas.width / gl.canvas.height, 0.01, 100); 58 | gl.matrixMode(gl.MODELVIEW); 59 | draw(); 60 | } 61 | 62 | document.body.appendChild(gl.canvas); 63 | gl.clearColor(0, 0, 0, 1); 64 | 65 | water = new Water(); 66 | renderer = new Renderer(); 67 | cubemap = new Cubemap({ 68 | xneg: document.getElementById('xneg'), 69 | xpos: document.getElementById('xpos'), 70 | yneg: document.getElementById('ypos'), 71 | ypos: document.getElementById('ypos'), 72 | zneg: document.getElementById('zneg'), 73 | zpos: document.getElementById('zpos') 74 | }); 75 | 76 | if (!water.textureA.canDrawTo() || !water.textureB.canDrawTo()) { 77 | throw new Error('Rendering to floating-point textures is required but not supported'); 78 | } 79 | 80 | center = oldCenter = new GL.Vector(-0.4, -0.75, 0.2); 81 | velocity = new GL.Vector(); 82 | gravity = new GL.Vector(0, -4, 0); 83 | radius = 0.25; 84 | 85 | for (var i = 0; i < 20; i++) { 86 | water.addDrop(Math.random() * 2 - 1, Math.random() * 2 - 1, 0.03, (i & 1) ? 0.01 : -0.01); 87 | } 88 | 89 | document.getElementById('loading').innerHTML = ''; 90 | onresize(); 91 | 92 | var requestAnimationFrame = 93 | window.requestAnimationFrame || 94 | window.webkitRequestAnimationFrame || 95 | function(callback) { setTimeout(callback, 0); }; 96 | 97 | var prevTime = new Date().getTime(); 98 | function animate() { 99 | var nextTime = new Date().getTime(); 100 | if (!paused) { 101 | update((nextTime - prevTime) / 1000); 102 | draw(); 103 | } 104 | prevTime = nextTime; 105 | requestAnimationFrame(animate); 106 | } 107 | requestAnimationFrame(animate); 108 | 109 | window.onresize = onresize; 110 | 111 | var prevHit; 112 | var planeNormal; 113 | var mode = -1; 114 | var MODE_ADD_DROPS = 0; 115 | var MODE_MOVE_SPHERE = 1; 116 | var MODE_ORBIT_CAMERA = 2; 117 | 118 | var oldX, oldY; 119 | 120 | function startDrag(x, y) { 121 | oldX = x; 122 | oldY = y; 123 | var tracer = new GL.Raytracer(); 124 | var ray = tracer.getRayForPixel(x * ratio, y * ratio); 125 | var pointOnPlane = tracer.eye.add(ray.multiply(-tracer.eye.y / ray.y)); 126 | var sphereHitTest = GL.Raytracer.hitTestSphere(tracer.eye, ray, center, radius); 127 | if (sphereHitTest) { 128 | mode = MODE_MOVE_SPHERE; 129 | prevHit = sphereHitTest.hit; 130 | planeNormal = tracer.getRayForPixel(gl.canvas.width / 2, gl.canvas.height / 2).negative(); 131 | } else if (Math.abs(pointOnPlane.x) < 1 && Math.abs(pointOnPlane.z) < 1) { 132 | mode = MODE_ADD_DROPS; 133 | duringDrag(x, y); 134 | } else { 135 | mode = MODE_ORBIT_CAMERA; 136 | } 137 | } 138 | 139 | function duringDrag(x, y) { 140 | switch (mode) { 141 | case MODE_ADD_DROPS: { 142 | var tracer = new GL.Raytracer(); 143 | var ray = tracer.getRayForPixel(x * ratio, y * ratio); 144 | var pointOnPlane = tracer.eye.add(ray.multiply(-tracer.eye.y / ray.y)); 145 | water.addDrop(pointOnPlane.x, pointOnPlane.z, 0.03, 0.01); 146 | if (paused) { 147 | water.updateNormals(); 148 | renderer.updateCaustics(water); 149 | } 150 | break; 151 | } 152 | case MODE_MOVE_SPHERE: { 153 | var tracer = new GL.Raytracer(); 154 | var ray = tracer.getRayForPixel(x * ratio, y * ratio); 155 | var t = -planeNormal.dot(tracer.eye.subtract(prevHit)) / planeNormal.dot(ray); 156 | var nextHit = tracer.eye.add(ray.multiply(t)); 157 | center = center.add(nextHit.subtract(prevHit)); 158 | center.x = Math.max(radius - 1, Math.min(1 - radius, center.x)); 159 | center.y = Math.max(radius - 1, Math.min(10, center.y)); 160 | center.z = Math.max(radius - 1, Math.min(1 - radius, center.z)); 161 | prevHit = nextHit; 162 | if (paused) renderer.updateCaustics(water); 163 | break; 164 | } 165 | case MODE_ORBIT_CAMERA: { 166 | angleY -= x - oldX; 167 | angleX -= y - oldY; 168 | angleX = Math.max(-89.999, Math.min(89.999, angleX)); 169 | break; 170 | } 171 | } 172 | oldX = x; 173 | oldY = y; 174 | if (paused) draw(); 175 | } 176 | 177 | function stopDrag() { 178 | mode = -1; 179 | } 180 | 181 | function isHelpElement(element) { 182 | return element === help || element.parentNode && isHelpElement(element.parentNode); 183 | } 184 | 185 | document.onmousedown = function(e) { 186 | if (!isHelpElement(e.target)) { 187 | e.preventDefault(); 188 | startDrag(e.pageX, e.pageY); 189 | } 190 | }; 191 | 192 | document.onmousemove = function(e) { 193 | duringDrag(e.pageX, e.pageY); 194 | }; 195 | 196 | document.onmouseup = function() { 197 | stopDrag(); 198 | }; 199 | 200 | document.ontouchstart = function(e) { 201 | if (e.touches.length === 1 && !isHelpElement(e.target)) { 202 | e.preventDefault(); 203 | startDrag(e.touches[0].pageX, e.touches[0].pageY); 204 | } 205 | }; 206 | 207 | document.ontouchmove = function(e) { 208 | if (e.touches.length === 1) { 209 | duringDrag(e.touches[0].pageX, e.touches[0].pageY); 210 | } 211 | }; 212 | 213 | document.ontouchend = function(e) { 214 | if (e.touches.length == 0) { 215 | stopDrag(); 216 | } 217 | }; 218 | 219 | document.onkeydown = function(e) { 220 | if (e.which == ' '.charCodeAt(0)) paused = !paused; 221 | else if (e.which == 'G'.charCodeAt(0)) useSpherePhysics = !useSpherePhysics; 222 | else if (e.which == 'L'.charCodeAt(0) && paused) draw(); 223 | }; 224 | 225 | var frame = 0; 226 | 227 | function update(seconds) { 228 | if (seconds > 1) return; 229 | frame += seconds * 2; 230 | 231 | if (mode == MODE_MOVE_SPHERE) { 232 | // Start from rest when the player releases the mouse after moving the sphere 233 | velocity = new GL.Vector(); 234 | } else if (useSpherePhysics) { 235 | // Fall down with viscosity under water 236 | var percentUnderWater = Math.max(0, Math.min(1, (radius - center.y) / (2 * radius))); 237 | velocity = velocity.add(gravity.multiply(seconds - 1.1 * seconds * percentUnderWater)); 238 | velocity = velocity.subtract(velocity.unit().multiply(percentUnderWater * seconds * velocity.dot(velocity))); 239 | center = center.add(velocity.multiply(seconds)); 240 | 241 | // Bounce off the bottom 242 | if (center.y < radius - 1) { 243 | center.y = radius - 1; 244 | velocity.y = Math.abs(velocity.y) * 0.7; 245 | } 246 | } 247 | 248 | // Displace water around the sphere 249 | water.moveSphere(oldCenter, center, radius); 250 | oldCenter = center; 251 | 252 | // Update the water simulation and graphics 253 | water.stepSimulation(); 254 | water.stepSimulation(); 255 | water.updateNormals(); 256 | renderer.updateCaustics(water); 257 | } 258 | 259 | function draw() { 260 | // Change the light direction to the camera look vector when the L key is pressed 261 | if (GL.keys.L) { 262 | renderer.lightDir = GL.Vector.fromAngles((90 - angleY) * Math.PI / 180, -angleX * Math.PI / 180); 263 | if (paused) renderer.updateCaustics(water); 264 | } 265 | 266 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 267 | gl.loadIdentity(); 268 | gl.translate(0, 0, -4); 269 | gl.rotate(-angleX, 1, 0, 0); 270 | gl.rotate(-angleY, 0, 1, 0); 271 | gl.translate(0, 0.5, 0); 272 | 273 | gl.enable(gl.DEPTH_TEST); 274 | renderer.sphereCenter = center; 275 | renderer.sphereRadius = radius; 276 | renderer.renderCube(); 277 | renderer.renderWater(water, cubemap); 278 | renderer.renderSphere(); 279 | gl.disable(gl.DEPTH_TEST); 280 | } 281 | }; 282 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WebGL Water 3 | * http://madebyevan.com/webgl-water/ 4 | * 5 | * Copyright 2011 Evan Wallace 6 | * Released under the MIT license 7 | */ 8 | 9 | var helperFunctions = '\ 10 | const float IOR_AIR = 1.0;\ 11 | const float IOR_WATER = 1.333;\ 12 | const vec3 abovewaterColor = vec3(0.25, 1.0, 1.25);\ 13 | const vec3 underwaterColor = vec3(0.4, 0.9, 1.0);\ 14 | const float poolHeight = 1.0;\ 15 | uniform vec3 light;\ 16 | uniform vec3 sphereCenter;\ 17 | uniform float sphereRadius;\ 18 | uniform sampler2D tiles;\ 19 | uniform sampler2D causticTex;\ 20 | uniform sampler2D water;\ 21 | \ 22 | vec2 intersectCube(vec3 origin, vec3 ray, vec3 cubeMin, vec3 cubeMax) {\ 23 | vec3 tMin = (cubeMin - origin) / ray;\ 24 | vec3 tMax = (cubeMax - origin) / ray;\ 25 | vec3 t1 = min(tMin, tMax);\ 26 | vec3 t2 = max(tMin, tMax);\ 27 | float tNear = max(max(t1.x, t1.y), t1.z);\ 28 | float tFar = min(min(t2.x, t2.y), t2.z);\ 29 | return vec2(tNear, tFar);\ 30 | }\ 31 | \ 32 | float intersectSphere(vec3 origin, vec3 ray, vec3 sphereCenter, float sphereRadius) {\ 33 | vec3 toSphere = origin - sphereCenter;\ 34 | float a = dot(ray, ray);\ 35 | float b = 2.0 * dot(toSphere, ray);\ 36 | float c = dot(toSphere, toSphere) - sphereRadius * sphereRadius;\ 37 | float discriminant = b*b - 4.0*a*c;\ 38 | if (discriminant > 0.0) {\ 39 | float t = (-b - sqrt(discriminant)) / (2.0 * a);\ 40 | if (t > 0.0) return t;\ 41 | }\ 42 | return 1.0e6;\ 43 | }\ 44 | \ 45 | vec3 getSphereColor(vec3 point) {\ 46 | vec3 color = vec3(0.5);\ 47 | \ 48 | /* ambient occlusion with walls */\ 49 | color *= 1.0 - 0.9 / pow((1.0 + sphereRadius - abs(point.x)) / sphereRadius, 3.0);\ 50 | color *= 1.0 - 0.9 / pow((1.0 + sphereRadius - abs(point.z)) / sphereRadius, 3.0);\ 51 | color *= 1.0 - 0.9 / pow((point.y + 1.0 + sphereRadius) / sphereRadius, 3.0);\ 52 | \ 53 | /* caustics */\ 54 | vec3 sphereNormal = (point - sphereCenter) / sphereRadius;\ 55 | vec3 refractedLight = refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);\ 56 | float diffuse = max(0.0, dot(-refractedLight, sphereNormal)) * 0.5;\ 57 | vec4 info = texture2D(water, point.xz * 0.5 + 0.5);\ 58 | if (point.y < info.r) {\ 59 | vec4 caustic = texture2D(causticTex, 0.75 * (point.xz - point.y * refractedLight.xz / refractedLight.y) * 0.5 + 0.5);\ 60 | diffuse *= caustic.r * 4.0;\ 61 | }\ 62 | color += diffuse;\ 63 | \ 64 | return color;\ 65 | }\ 66 | \ 67 | vec3 getWallColor(vec3 point) {\ 68 | float scale = 0.5;\ 69 | \ 70 | vec3 wallColor;\ 71 | vec3 normal;\ 72 | if (abs(point.x) > 0.999) {\ 73 | wallColor = texture2D(tiles, point.yz * 0.5 + vec2(1.0, 0.5)).rgb;\ 74 | normal = vec3(-point.x, 0.0, 0.0);\ 75 | } else if (abs(point.z) > 0.999) {\ 76 | wallColor = texture2D(tiles, point.yx * 0.5 + vec2(1.0, 0.5)).rgb;\ 77 | normal = vec3(0.0, 0.0, -point.z);\ 78 | } else {\ 79 | wallColor = texture2D(tiles, point.xz * 0.5 + 0.5).rgb;\ 80 | normal = vec3(0.0, 1.0, 0.0);\ 81 | }\ 82 | \ 83 | scale /= length(point); /* pool ambient occlusion */\ 84 | scale *= 1.0 - 0.9 / pow(length(point - sphereCenter) / sphereRadius, 4.0); /* sphere ambient occlusion */\ 85 | \ 86 | /* caustics */\ 87 | vec3 refractedLight = -refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);\ 88 | float diffuse = max(0.0, dot(refractedLight, normal));\ 89 | vec4 info = texture2D(water, point.xz * 0.5 + 0.5);\ 90 | if (point.y < info.r) {\ 91 | vec4 caustic = texture2D(causticTex, 0.75 * (point.xz - point.y * refractedLight.xz / refractedLight.y) * 0.5 + 0.5);\ 92 | scale += diffuse * caustic.r * 2.0 * caustic.g;\ 93 | } else {\ 94 | /* shadow for the rim of the pool */\ 95 | vec2 t = intersectCube(point, refractedLight, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));\ 96 | diffuse *= 1.0 / (1.0 + exp(-200.0 / (1.0 + 10.0 * (t.y - t.x)) * (point.y + refractedLight.y * t.y - 2.0 / 12.0)));\ 97 | \ 98 | scale += diffuse * 0.5;\ 99 | }\ 100 | \ 101 | return wallColor * scale;\ 102 | }\ 103 | '; 104 | 105 | function Renderer() { 106 | this.tileTexture = GL.Texture.fromImage(document.getElementById('tiles'), { 107 | minFilter: gl.LINEAR_MIPMAP_LINEAR, 108 | wrap: gl.REPEAT, 109 | format: gl.RGB 110 | }); 111 | this.lightDir = new GL.Vector(2.0, 2.0, -1.0).unit(); 112 | this.causticTex = new GL.Texture(1024, 1024); 113 | this.waterMesh = GL.Mesh.plane({ detail: 200 }); 114 | this.waterShaders = []; 115 | for (var i = 0; i < 2; i++) { 116 | this.waterShaders[i] = new GL.Shader('\ 117 | uniform sampler2D water;\ 118 | varying vec3 position;\ 119 | void main() {\ 120 | vec4 info = texture2D(water, gl_Vertex.xy * 0.5 + 0.5);\ 121 | position = gl_Vertex.xzy;\ 122 | position.y += info.r;\ 123 | gl_Position = gl_ModelViewProjectionMatrix * vec4(position, 1.0);\ 124 | }\ 125 | ', helperFunctions + '\ 126 | uniform vec3 eye;\ 127 | varying vec3 position;\ 128 | uniform samplerCube sky;\ 129 | \ 130 | vec3 getSurfaceRayColor(vec3 origin, vec3 ray, vec3 waterColor) {\ 131 | vec3 color;\ 132 | float q = intersectSphere(origin, ray, sphereCenter, sphereRadius);\ 133 | if (q < 1.0e6) {\ 134 | color = getSphereColor(origin + ray * q);\ 135 | } else if (ray.y < 0.0) {\ 136 | vec2 t = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));\ 137 | color = getWallColor(origin + ray * t.y);\ 138 | } else {\ 139 | vec2 t = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));\ 140 | vec3 hit = origin + ray * t.y;\ 141 | if (hit.y < 2.0 / 12.0) {\ 142 | color = getWallColor(hit);\ 143 | } else {\ 144 | color = textureCube(sky, ray).rgb;\ 145 | color += vec3(pow(max(0.0, dot(light, ray)), 5000.0)) * vec3(10.0, 8.0, 6.0);\ 146 | }\ 147 | }\ 148 | if (ray.y < 0.0) color *= waterColor;\ 149 | return color;\ 150 | }\ 151 | \ 152 | void main() {\ 153 | vec2 coord = position.xz * 0.5 + 0.5;\ 154 | vec4 info = texture2D(water, coord);\ 155 | \ 156 | /* make water look more "peaked" */\ 157 | for (int i = 0; i < 5; i++) {\ 158 | coord += info.ba * 0.005;\ 159 | info = texture2D(water, coord);\ 160 | }\ 161 | \ 162 | vec3 normal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a);\ 163 | vec3 incomingRay = normalize(position - eye);\ 164 | \ 165 | ' + (i ? /* underwater */ '\ 166 | normal = -normal;\ 167 | vec3 reflectedRay = reflect(incomingRay, normal);\ 168 | vec3 refractedRay = refract(incomingRay, normal, IOR_WATER / IOR_AIR);\ 169 | float fresnel = mix(0.5, 1.0, pow(1.0 - dot(normal, -incomingRay), 3.0));\ 170 | \ 171 | vec3 reflectedColor = getSurfaceRayColor(position, reflectedRay, underwaterColor);\ 172 | vec3 refractedColor = getSurfaceRayColor(position, refractedRay, vec3(1.0)) * vec3(0.8, 1.0, 1.1);\ 173 | \ 174 | gl_FragColor = vec4(mix(reflectedColor, refractedColor, (1.0 - fresnel) * length(refractedRay)), 1.0);\ 175 | ' : /* above water */ '\ 176 | vec3 reflectedRay = reflect(incomingRay, normal);\ 177 | vec3 refractedRay = refract(incomingRay, normal, IOR_AIR / IOR_WATER);\ 178 | float fresnel = mix(0.25, 1.0, pow(1.0 - dot(normal, -incomingRay), 3.0));\ 179 | \ 180 | vec3 reflectedColor = getSurfaceRayColor(position, reflectedRay, abovewaterColor);\ 181 | vec3 refractedColor = getSurfaceRayColor(position, refractedRay, abovewaterColor);\ 182 | \ 183 | gl_FragColor = vec4(mix(refractedColor, reflectedColor, fresnel), 1.0);\ 184 | ') + '\ 185 | }\ 186 | '); 187 | } 188 | this.sphereMesh = GL.Mesh.sphere({ detail: 10 }); 189 | this.sphereShader = new GL.Shader(helperFunctions + '\ 190 | varying vec3 position;\ 191 | void main() {\ 192 | position = sphereCenter + gl_Vertex.xyz * sphereRadius;\ 193 | gl_Position = gl_ModelViewProjectionMatrix * vec4(position, 1.0);\ 194 | }\ 195 | ', helperFunctions + '\ 196 | varying vec3 position;\ 197 | void main() {\ 198 | gl_FragColor = vec4(getSphereColor(position), 1.0);\ 199 | vec4 info = texture2D(water, position.xz * 0.5 + 0.5);\ 200 | if (position.y < info.r) {\ 201 | gl_FragColor.rgb *= underwaterColor * 1.2;\ 202 | }\ 203 | }\ 204 | '); 205 | this.cubeMesh = GL.Mesh.cube(); 206 | this.cubeMesh.triangles.splice(4, 2); 207 | this.cubeMesh.compile(); 208 | this.cubeShader = new GL.Shader(helperFunctions + '\ 209 | varying vec3 position;\ 210 | void main() {\ 211 | position = gl_Vertex.xyz;\ 212 | position.y = ((1.0 - position.y) * (7.0 / 12.0) - 1.0) * poolHeight;\ 213 | gl_Position = gl_ModelViewProjectionMatrix * vec4(position, 1.0);\ 214 | }\ 215 | ', helperFunctions + '\ 216 | varying vec3 position;\ 217 | void main() {\ 218 | gl_FragColor = vec4(getWallColor(position), 1.0);\ 219 | vec4 info = texture2D(water, position.xz * 0.5 + 0.5);\ 220 | if (position.y < info.r) {\ 221 | gl_FragColor.rgb *= underwaterColor * 1.2;\ 222 | }\ 223 | }\ 224 | '); 225 | this.sphereCenter = new GL.Vector(); 226 | this.sphereRadius = 0; 227 | var hasDerivatives = !!gl.getExtension('OES_standard_derivatives'); 228 | this.causticsShader = new GL.Shader(helperFunctions + '\ 229 | varying vec3 oldPos;\ 230 | varying vec3 newPos;\ 231 | varying vec3 ray;\ 232 | \ 233 | /* project the ray onto the plane */\ 234 | vec3 project(vec3 origin, vec3 ray, vec3 refractedLight) {\ 235 | vec2 tcube = intersectCube(origin, ray, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));\ 236 | origin += ray * tcube.y;\ 237 | float tplane = (-origin.y - 1.0) / refractedLight.y;\ 238 | return origin + refractedLight * tplane;\ 239 | }\ 240 | \ 241 | void main() {\ 242 | vec4 info = texture2D(water, gl_Vertex.xy * 0.5 + 0.5);\ 243 | info.ba *= 0.5;\ 244 | vec3 normal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a);\ 245 | \ 246 | /* project the vertices along the refracted vertex ray */\ 247 | vec3 refractedLight = refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);\ 248 | ray = refract(-light, normal, IOR_AIR / IOR_WATER);\ 249 | oldPos = project(gl_Vertex.xzy, refractedLight, refractedLight);\ 250 | newPos = project(gl_Vertex.xzy + vec3(0.0, info.r, 0.0), ray, refractedLight);\ 251 | \ 252 | gl_Position = vec4(0.75 * (newPos.xz + refractedLight.xz / refractedLight.y), 0.0, 1.0);\ 253 | }\ 254 | ', (hasDerivatives ? '#extension GL_OES_standard_derivatives : enable\n' : '') + '\ 255 | ' + helperFunctions + '\ 256 | varying vec3 oldPos;\ 257 | varying vec3 newPos;\ 258 | varying vec3 ray;\ 259 | \ 260 | void main() {\ 261 | ' + (hasDerivatives ? '\ 262 | /* if the triangle gets smaller, it gets brighter, and vice versa */\ 263 | float oldArea = length(dFdx(oldPos)) * length(dFdy(oldPos));\ 264 | float newArea = length(dFdx(newPos)) * length(dFdy(newPos));\ 265 | gl_FragColor = vec4(oldArea / newArea * 0.2, 1.0, 0.0, 0.0);\ 266 | ' : '\ 267 | gl_FragColor = vec4(0.2, 0.2, 0.0, 0.0);\ 268 | ' ) + '\ 269 | \ 270 | vec3 refractedLight = refract(-light, vec3(0.0, 1.0, 0.0), IOR_AIR / IOR_WATER);\ 271 | \ 272 | /* compute a blob shadow and make sure we only draw a shadow if the player is blocking the light */\ 273 | vec3 dir = (sphereCenter - newPos) / sphereRadius;\ 274 | vec3 area = cross(dir, refractedLight);\ 275 | float shadow = dot(area, area);\ 276 | float dist = dot(dir, -refractedLight);\ 277 | shadow = 1.0 + (shadow - 1.0) / (0.05 + dist * 0.025);\ 278 | shadow = clamp(1.0 / (1.0 + exp(-shadow)), 0.0, 1.0);\ 279 | shadow = mix(1.0, shadow, clamp(dist * 2.0, 0.0, 1.0));\ 280 | gl_FragColor.g = shadow;\ 281 | \ 282 | /* shadow for the rim of the pool */\ 283 | vec2 t = intersectCube(newPos, -refractedLight, vec3(-1.0, -poolHeight, -1.0), vec3(1.0, 2.0, 1.0));\ 284 | gl_FragColor.r *= 1.0 / (1.0 + exp(-200.0 / (1.0 + 10.0 * (t.y - t.x)) * (newPos.y - refractedLight.y * t.y - 2.0 / 12.0)));\ 285 | }\ 286 | '); 287 | } 288 | 289 | Renderer.prototype.updateCaustics = function(water) { 290 | if (!this.causticsShader) return; 291 | var this_ = this; 292 | this.causticTex.drawTo(function() { 293 | gl.clear(gl.COLOR_BUFFER_BIT); 294 | water.textureA.bind(0); 295 | this_.causticsShader.uniforms({ 296 | light: this_.lightDir, 297 | water: 0, 298 | sphereCenter: this_.sphereCenter, 299 | sphereRadius: this_.sphereRadius 300 | }).draw(this_.waterMesh); 301 | }); 302 | }; 303 | 304 | Renderer.prototype.renderWater = function(water, sky) { 305 | var tracer = new GL.Raytracer(); 306 | water.textureA.bind(0); 307 | this.tileTexture.bind(1); 308 | sky.bind(2); 309 | this.causticTex.bind(3); 310 | gl.enable(gl.CULL_FACE); 311 | for (var i = 0; i < 2; i++) { 312 | gl.cullFace(i ? gl.BACK : gl.FRONT); 313 | this.waterShaders[i].uniforms({ 314 | light: this.lightDir, 315 | water: 0, 316 | tiles: 1, 317 | sky: 2, 318 | causticTex: 3, 319 | eye: tracer.eye, 320 | sphereCenter: this.sphereCenter, 321 | sphereRadius: this.sphereRadius 322 | }).draw(this.waterMesh); 323 | } 324 | gl.disable(gl.CULL_FACE); 325 | }; 326 | 327 | Renderer.prototype.renderSphere = function() { 328 | water.textureA.bind(0); 329 | this.causticTex.bind(1); 330 | this.sphereShader.uniforms({ 331 | light: this.lightDir, 332 | water: 0, 333 | causticTex: 1, 334 | sphereCenter: this.sphereCenter, 335 | sphereRadius: this.sphereRadius 336 | }).draw(this.sphereMesh); 337 | }; 338 | 339 | Renderer.prototype.renderCube = function() { 340 | gl.enable(gl.CULL_FACE); 341 | water.textureA.bind(0); 342 | this.tileTexture.bind(1); 343 | this.causticTex.bind(2); 344 | this.cubeShader.uniforms({ 345 | light: this.lightDir, 346 | water: 0, 347 | tiles: 1, 348 | causticTex: 2, 349 | sphereCenter: this.sphereCenter, 350 | sphereRadius: this.sphereRadius 351 | }).draw(this.cubeMesh); 352 | gl.disable(gl.CULL_FACE); 353 | }; 354 | -------------------------------------------------------------------------------- /tiles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/tiles.jpg -------------------------------------------------------------------------------- /water.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WebGL Water 3 | * http://madebyevan.com/webgl-water/ 4 | * 5 | * Copyright 2011 Evan Wallace 6 | * Released under the MIT license 7 | */ 8 | 9 | // The data in the texture is (position.y, velocity.y, normal.x, normal.z) 10 | function Water() { 11 | var vertexShader = '\ 12 | varying vec2 coord;\ 13 | void main() {\ 14 | coord = gl_Vertex.xy * 0.5 + 0.5;\ 15 | gl_Position = vec4(gl_Vertex.xyz, 1.0);\ 16 | }\ 17 | '; 18 | this.plane = GL.Mesh.plane(); 19 | if (!GL.Texture.canUseFloatingPointTextures()) { 20 | throw new Error('This demo requires the OES_texture_float extension'); 21 | } 22 | var filter = GL.Texture.canUseFloatingPointLinearFiltering() ? gl.LINEAR : gl.NEAREST; 23 | this.textureA = new GL.Texture(256, 256, { type: gl.FLOAT, filter: filter }); 24 | this.textureB = new GL.Texture(256, 256, { type: gl.FLOAT, filter: filter }); 25 | if ((!this.textureA.canDrawTo() || !this.textureB.canDrawTo()) && GL.Texture.canUseHalfFloatingPointTextures()) { 26 | filter = GL.Texture.canUseHalfFloatingPointLinearFiltering() ? gl.LINEAR : gl.NEAREST; 27 | this.textureA = new GL.Texture(256, 256, { type: gl.HALF_FLOAT_OES, filter: filter }); 28 | this.textureB = new GL.Texture(256, 256, { type: gl.HALF_FLOAT_OES, filter: filter }); 29 | } 30 | this.dropShader = new GL.Shader(vertexShader, '\ 31 | const float PI = 3.141592653589793;\ 32 | uniform sampler2D texture;\ 33 | uniform vec2 center;\ 34 | uniform float radius;\ 35 | uniform float strength;\ 36 | varying vec2 coord;\ 37 | void main() {\ 38 | /* get vertex info */\ 39 | vec4 info = texture2D(texture, coord);\ 40 | \ 41 | /* add the drop to the height */\ 42 | float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);\ 43 | drop = 0.5 - cos(drop * PI) * 0.5;\ 44 | info.r += drop * strength;\ 45 | \ 46 | gl_FragColor = info;\ 47 | }\ 48 | '); 49 | this.updateShader = new GL.Shader(vertexShader, '\ 50 | uniform sampler2D texture;\ 51 | uniform vec2 delta;\ 52 | varying vec2 coord;\ 53 | void main() {\ 54 | /* get vertex info */\ 55 | vec4 info = texture2D(texture, coord);\ 56 | \ 57 | /* calculate average neighbor height */\ 58 | vec2 dx = vec2(delta.x, 0.0);\ 59 | vec2 dy = vec2(0.0, delta.y);\ 60 | float average = (\ 61 | texture2D(texture, coord - dx).r +\ 62 | texture2D(texture, coord - dy).r +\ 63 | texture2D(texture, coord + dx).r +\ 64 | texture2D(texture, coord + dy).r\ 65 | ) * 0.25;\ 66 | \ 67 | /* change the velocity to move toward the average */\ 68 | info.g += (average - info.r) * 2.0;\ 69 | \ 70 | /* attenuate the velocity a little so waves do not last forever */\ 71 | info.g *= 0.995;\ 72 | \ 73 | /* move the vertex along the velocity */\ 74 | info.r += info.g;\ 75 | \ 76 | gl_FragColor = info;\ 77 | }\ 78 | '); 79 | this.normalShader = new GL.Shader(vertexShader, '\ 80 | uniform sampler2D texture;\ 81 | uniform vec2 delta;\ 82 | varying vec2 coord;\ 83 | void main() {\ 84 | /* get vertex info */\ 85 | vec4 info = texture2D(texture, coord);\ 86 | \ 87 | /* update the normal */\ 88 | vec3 dx = vec3(delta.x, texture2D(texture, vec2(coord.x + delta.x, coord.y)).r - info.r, 0.0);\ 89 | vec3 dy = vec3(0.0, texture2D(texture, vec2(coord.x, coord.y + delta.y)).r - info.r, delta.y);\ 90 | info.ba = normalize(cross(dy, dx)).xz;\ 91 | \ 92 | gl_FragColor = info;\ 93 | }\ 94 | '); 95 | this.sphereShader = new GL.Shader(vertexShader, '\ 96 | uniform sampler2D texture;\ 97 | uniform vec3 oldCenter;\ 98 | uniform vec3 newCenter;\ 99 | uniform float radius;\ 100 | varying vec2 coord;\ 101 | \ 102 | float volumeInSphere(vec3 center) {\ 103 | vec3 toCenter = vec3(coord.x * 2.0 - 1.0, 0.0, coord.y * 2.0 - 1.0) - center;\ 104 | float t = length(toCenter) / radius;\ 105 | float dy = exp(-pow(t * 1.5, 6.0));\ 106 | float ymin = min(0.0, center.y - dy);\ 107 | float ymax = min(max(0.0, center.y + dy), ymin + 2.0 * dy);\ 108 | return (ymax - ymin) * 0.1;\ 109 | }\ 110 | \ 111 | void main() {\ 112 | /* get vertex info */\ 113 | vec4 info = texture2D(texture, coord);\ 114 | \ 115 | /* add the old volume */\ 116 | info.r += volumeInSphere(oldCenter);\ 117 | \ 118 | /* subtract the new volume */\ 119 | info.r -= volumeInSphere(newCenter);\ 120 | \ 121 | gl_FragColor = info;\ 122 | }\ 123 | '); 124 | } 125 | 126 | Water.prototype.addDrop = function(x, y, radius, strength) { 127 | var this_ = this; 128 | this.textureB.drawTo(function() { 129 | this_.textureA.bind(); 130 | this_.dropShader.uniforms({ 131 | center: [x, y], 132 | radius: radius, 133 | strength: strength 134 | }).draw(this_.plane); 135 | }); 136 | this.textureB.swapWith(this.textureA); 137 | }; 138 | 139 | Water.prototype.moveSphere = function(oldCenter, newCenter, radius) { 140 | var this_ = this; 141 | this.textureB.drawTo(function() { 142 | this_.textureA.bind(); 143 | this_.sphereShader.uniforms({ 144 | oldCenter: oldCenter, 145 | newCenter: newCenter, 146 | radius: radius 147 | }).draw(this_.plane); 148 | }); 149 | this.textureB.swapWith(this.textureA); 150 | }; 151 | 152 | Water.prototype.stepSimulation = function() { 153 | var this_ = this; 154 | this.textureB.drawTo(function() { 155 | this_.textureA.bind(); 156 | this_.updateShader.uniforms({ 157 | delta: [1 / this_.textureA.width, 1 / this_.textureA.height] 158 | }).draw(this_.plane); 159 | }); 160 | this.textureB.swapWith(this.textureA); 161 | }; 162 | 163 | Water.prototype.updateNormals = function() { 164 | var this_ = this; 165 | this.textureB.drawTo(function() { 166 | this_.textureA.bind(); 167 | this_.normalShader.uniforms({ 168 | delta: [1 / this_.textureA.width, 1 / this_.textureA.height] 169 | }).draw(this_.plane); 170 | }); 171 | this.textureB.swapWith(this.textureA); 172 | }; 173 | -------------------------------------------------------------------------------- /xneg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/xneg.jpg -------------------------------------------------------------------------------- /xpos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/xpos.jpg -------------------------------------------------------------------------------- /ypos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/ypos.jpg -------------------------------------------------------------------------------- /zneg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/zneg.jpg -------------------------------------------------------------------------------- /zpos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/webgl-water/73eda8be832b649367b25ea5690c1f0181bb56ad/zpos.jpg --------------------------------------------------------------------------------