├── 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 |
43 | - Draw on the water to make ripples
44 | - Drag the background to rotate the camera
45 | - Press SPACEBAR to pause and unpause
46 | - Drag the sphere to move it around
47 | - Press the L key to set the light direction
48 | - Press the G key to toggle gravity
49 |
50 |
Features:
51 |
52 | - Raytraced reflections and refractions
53 | - Analytic ambient occlusion
54 | - Heightfield water simulation *
55 | - Soft shadows
56 | - Caustics (see this for details) **
57 |
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
--------------------------------------------------------------------------------