├── .gitignore
├── demo
├── css
│ └── index.css
├── lib
│ ├── copy
│ │ ├── copy.vert
│ │ ├── copy.frag
│ │ └── copy.js
│ ├── floor.js
│ ├── controls.js
│ └── fpsmeter.js
├── shaders
│ ├── deferred-lightning.vert
│ ├── cache-for-deferred.frag
│ ├── cache-for-deferred-w-tex.frag
│ ├── cache-for-deferred.vert
│ ├── cache-for-deferred-w-tex.vert
│ └── deferred-lightning.frag
├── index.html
├── options.js
└── index.js
├── src
├── lib
│ ├── fragment.glsl
│ ├── fbo.glsl
│ ├── coord-transforms.glsl
│ ├── get-fragment.glsl
│ └── march-ray.glsl
├── screen-space-reflections.vert
└── screen-space-reflections.frag
├── test
├── index.html
├── shaders
│ ├── test.vert
│ ├── test-ray-trace.vert
│ ├── test.frag
│ ├── cache-for-deferred.vert
│ ├── cache-for-deferred.frag
│ └── test-ray-trace.frag
├── test-case.js
├── test-point.js
├── test-config.js
├── lib
│ └── gl-util.js
└── test.js
├── lib
└── view-aligned-square.js
├── Gruntfile.js
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | demo/textures
4 | bundle.js
5 | *.txt
6 | *.DS_Store
7 | *Icon*
8 | *.gz
9 | *.jp*g
10 | *.png
--------------------------------------------------------------------------------
/demo/css/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0px;
3 | overflow: hidden;
4 | }
5 |
6 | .full-screen {
7 | width: 100vw;
8 | height: 100vh;
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/fragment.glsl:
--------------------------------------------------------------------------------
1 | struct Fragment {
2 | vec4 color;
3 | bool isSpecular;
4 | bool isValid;
5 | vec3 normal;
6 | float reciprocalZ;
7 | vec3 viewPos;
8 | };
--------------------------------------------------------------------------------
/src/lib/fbo.glsl:
--------------------------------------------------------------------------------
1 | struct FBO {
2 | sampler2D colorSampler;
3 | sampler2D isSpecularSampler;
4 | sampler2D normalSampler;
5 | vec2 size;
6 | sampler2D viewPosSampler;
7 | };
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/demo/lib/copy/copy.vert:
--------------------------------------------------------------------------------
1 | attribute vec4 aPos;
2 | attribute vec2 aTexCoord;
3 |
4 | varying vec2 vTexCo;
5 |
6 | void main(void)
7 | {
8 | vTexCo = aTexCoord;
9 |
10 | gl_Position = aPos;
11 | }
--------------------------------------------------------------------------------
/demo/lib/copy/copy.frag:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | uniform sampler2D uTexture;
4 |
5 | varying vec2 vTexCo;
6 |
7 | void main(void)
8 | {
9 | gl_FragColor = texture2D(uTexture, vTexCo);
10 | }
--------------------------------------------------------------------------------
/test/shaders/test.vert:
--------------------------------------------------------------------------------
1 | attribute vec3 aPos;
2 | attribute vec2 aTexCoord;
3 |
4 | varying vec2 vTexCo;
5 |
6 | void main(void)
7 | {
8 | vTexCo = aTexCoord;
9 |
10 | gl_Position = vec4(aPos, 1.0);
11 | }
--------------------------------------------------------------------------------
/src/screen-space-reflections.vert:
--------------------------------------------------------------------------------
1 | attribute vec2 aTexCo;
2 |
3 | attribute vec3 aPos;
4 |
5 | varying vec2 vTexCo;
6 |
7 | void main()
8 | {
9 | vTexCo = aTexCo;
10 |
11 | gl_Position = vec4(aPos, 1.0);
12 | }
--------------------------------------------------------------------------------
/test/shaders/test-ray-trace.vert:
--------------------------------------------------------------------------------
1 | attribute vec2 aTexCo;
2 |
3 | attribute vec3 aPos;
4 |
5 | varying vec2 vTexCo;
6 |
7 | void main()
8 | {
9 | vTexCo = aTexCo;
10 |
11 | gl_Position = vec4(aPos, 1.0);
12 | }
--------------------------------------------------------------------------------
/demo/shaders/deferred-lightning.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute vec4 aPos;
4 | attribute vec2 aTexCo;
5 |
6 | varying vec2 vTexCo;
7 |
8 | void main(void)
9 | {
10 | vTexCo = aTexCo;
11 |
12 | gl_Position = aPos;
13 | }
--------------------------------------------------------------------------------
/src/lib/coord-transforms.glsl:
--------------------------------------------------------------------------------
1 | vec2 screenSpaceToTexco(in vec2 screensPaceCoord, in vec2 bufferSize)
2 | {
3 | return (screensPaceCoord + bufferSize * 0.5) / bufferSize;
4 | }
5 |
6 | vec2 toScreenSpaceCoord(in vec2 normalizedDeviceCoord, in vec2 bufferSize)
7 | {
8 | return bufferSize * 0.5 * normalizedDeviceCoord;
9 | }
--------------------------------------------------------------------------------
/demo/lib/floor.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | cells: [
3 | [0, 1, 2],
4 | [2, 3, 0]
5 | ],
6 | positions: [
7 | [-100, 0, -100],
8 | [-100, 0, 100],
9 | [100, 0, 100],
10 | [100, 0, -100]
11 | ],
12 | texCos: [
13 | [0, 0],
14 | [10, 0],
15 | [10, 10],
16 | [0, 10]
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/test/shaders/test.frag:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | varying vec2 vTexCo;
4 |
5 | struct FBO {
6 | sampler2D colorSampler;
7 | sampler2D isSpecularSampler;
8 | sampler2D normalSampler;
9 | sampler2D viewPosSampler;
10 | };
11 |
12 | uniform FBO uFbo;
13 |
14 | void main(void)
15 | {
16 | gl_FragColor = texture2D(uFbo.colorSampler, vTexCo);;
17 | }
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/options.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | color1: '#ffae23',
3 | emphasizeReflections: true,
4 | floor: {
5 | shininess: 20,
6 | specularColor: '#ffffff'
7 | },
8 | lights: {
9 | ambientColor: '#545454',
10 | posX: -100,
11 | posY: 100,
12 | posZ: 100
13 | },
14 | reflectionsOn: true,
15 | teapot: {
16 | shininess: 1,
17 | specularColor: '#E6E6E6'
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/shaders/cache-for-deferred.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute float
4 | aIsSpecular,
5 | aPointSize;
6 | attribute vec3
7 | aNormal,
8 | aPos;
9 | attribute vec4 aColor;
10 |
11 | uniform mat4 uProjection;
12 |
13 | varying float vIsSpecular;
14 |
15 | varying vec3
16 | vNormal,
17 | vViewPos;
18 |
19 | varying vec4 vColor;
20 |
21 | void main()
22 | {
23 | vColor = aColor;
24 |
25 | vIsSpecular = aIsSpecular;
26 |
27 | vNormal = aNormal;
28 |
29 | vViewPos = aPos;
30 |
31 | gl_PointSize = aPointSize;
32 | //gl_PointSize = 4.0;
33 |
34 | gl_Position = uProjection * vec4(aPos, 1.0);
35 | }
--------------------------------------------------------------------------------
/demo/shaders/cache-for-deferred.frag:
--------------------------------------------------------------------------------
1 | #extension GL_EXT_draw_buffers : require
2 |
3 | precision highp float;
4 |
5 | uniform float
6 | uUseDiffuseLightning,
7 | uShininess;
8 |
9 | uniform vec3
10 | uDiffuseColor,
11 | uSpecularColor;
12 |
13 | varying vec3
14 | vNormal,
15 | vViewPos;
16 |
17 | void main()
18 | {
19 | gl_FragData[0] = vec4(vViewPos, 1.0);
20 | gl_FragData[1] = vec4(vNormal, 0.0);
21 | gl_FragData[2] = vec4(uDiffuseColor, uUseDiffuseLightning);
22 | //gl_FragData[2] = vec4(1.0, 0.0, 0.0, 1.0);
23 | gl_FragData[3] = vec4(uSpecularColor, uShininess);
24 | //gl_FragData[3] = vec4(1.0, 0.0, 0.0, 1.0);
25 |
26 | }
--------------------------------------------------------------------------------
/test/shaders/cache-for-deferred.frag:
--------------------------------------------------------------------------------
1 | #extension GL_EXT_draw_buffers : require
2 |
3 | precision highp float;
4 |
5 | varying float vIsSpecular;
6 |
7 | varying vec3
8 | vNormal,
9 | vViewPos;
10 |
11 | varying vec4 vColor;
12 |
13 | float weight;
14 |
15 | vec2 normPC;
16 |
17 | void main()
18 | {
19 | // Fade out colors towards the edges of the point to see where the ray hit
20 | // them.
21 | normPC = 2.0 * (gl_PointCoord - 0.5);
22 | weight = 1.0 - length(normPC);
23 |
24 | gl_FragData[0] = vColor * weight;
25 | gl_FragData[1] = vec4(vViewPos, 1.0);
26 | gl_FragData[2] = vec4(vNormal, 0.0);
27 | gl_FragData[3] = vec4(vIsSpecular);
28 | }
--------------------------------------------------------------------------------
/demo/shaders/cache-for-deferred-w-tex.frag:
--------------------------------------------------------------------------------
1 | #extension GL_EXT_draw_buffers : require
2 |
3 | precision highp float;
4 |
5 | uniform float
6 | uUseDiffuseLightning,
7 | uShininess;
8 |
9 | uniform sampler2D uTexture;
10 |
11 | uniform vec3 uSpecularColor;
12 |
13 | varying vec2 vTexCo;
14 |
15 | varying vec3
16 | vNormal,
17 | vViewPos;
18 |
19 | vec3 textureColor;
20 |
21 | void main()
22 | {
23 | textureColor = texture2D(uTexture, vTexCo).rgb;
24 |
25 | gl_FragData[0] = vec4(vViewPos, 1.0);
26 | gl_FragData[1] = vec4(vNormal, 0.0);
27 | gl_FragData[2] = vec4(textureColor, uUseDiffuseLightning);
28 | gl_FragData[3] = vec4(uSpecularColor, uShininess);
29 | }
--------------------------------------------------------------------------------
/lib/view-aligned-square.js:
--------------------------------------------------------------------------------
1 | // Sets up the geometry for a view-aligned square with texture coordinates.
2 |
3 | 'use strict'
4 |
5 | var glGeometry = require('gl-geometry')
6 |
7 | module.exports = function createVAS (gl, vertexAttrName, texCoordAttrName) {
8 | var geometry = glGeometry(gl),
9 |
10 | vertices = [
11 | -1, -1, 0,
12 | 1, -1, 0,
13 | 1, 1, 0,
14 | -1, 1, 0
15 | ],
16 | texCoords = [
17 | 0, 0,
18 | 1, 0,
19 | 1, 1,
20 | 0, 1
21 | ],
22 | indices = [
23 | 0, 1, 2,
24 | 2, 3, 0
25 | ]
26 |
27 | geometry.attr(vertexAttrName, vertices)
28 | geometry.attr(texCoordAttrName, texCoords, {size: 2})
29 | geometry.faces(indices)
30 |
31 | return geometry
32 | }
33 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | grunt.initConfig({
3 | pkg: grunt.file.readJSON('package.json'),
4 |
5 | browserify: {
6 | build: {
7 | src: './src/warp.js',
8 | dest: './dist/vr-warp.js'
9 | }
10 | },
11 |
12 | standard: {
13 | app: {
14 | src: ['*.js']
15 | }
16 | },
17 |
18 | uglify: {
19 | options: {
20 | sourceMap: true
21 | },
22 | build: {
23 | src: './dist/vr-warp.js',
24 | dest: './dist/vr-warp.min.js'
25 | }
26 | }
27 |
28 | })
29 |
30 | grunt.loadNpmTasks('grunt-browserify')
31 | grunt.loadNpmTasks('grunt-standard')
32 | grunt.loadNpmTasks('grunt-contrib-uglify')
33 |
34 | grunt.registerTask('default', ['standard', 'browserify', 'uglify'])
35 | }
36 |
--------------------------------------------------------------------------------
/demo/shaders/cache-for-deferred.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | #pragma glslify: transpose = require('glsl-transpose')
4 | #pragma glslify: inverse = require('glsl-inverse')
5 |
6 | attribute vec3
7 | aPos,
8 | aNormal;
9 |
10 | uniform mat4
11 | uModel,
12 | uProjection,
13 | uView;
14 |
15 | varying vec3
16 | vNormal,
17 | vViewPos;
18 |
19 | mat3 normalMatrix;
20 |
21 | mat4 modelViewMatrix;
22 |
23 | vec4 viewPos;
24 |
25 | // TODO (abiro) Move normal matrix computation to CPU.
26 | void main()
27 | {
28 | modelViewMatrix = uView * uModel;
29 |
30 | normalMatrix = transpose(inverse(mat3(modelViewMatrix)));
31 |
32 | vNormal = normalize(normalMatrix * aNormal);
33 |
34 | viewPos = modelViewMatrix * vec4(aPos, 1.0);
35 |
36 | vViewPos = viewPos.xyz;
37 |
38 | gl_Position = uProjection * viewPos;
39 | }
--------------------------------------------------------------------------------
/test/shaders/test-ray-trace.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | #pragma glslify: import('../../src/lib/fbo.glsl')
4 | #pragma glslify: import('../../src/lib/fragment.glsl')
5 |
6 | #pragma glslify: import('../../src/lib/coord-transforms.glsl')
7 | #pragma glslify: import('../../src/lib/get-fragment.glsl')
8 | #pragma glslify: import('../../src/lib/march-ray.glsl')
9 |
10 | uniform mat4 uProjection;
11 |
12 | varying vec2 vTexCo;
13 |
14 | uniform FBO uFbo;
15 |
16 | Fragment
17 | fragment,
18 | nextFragment;
19 |
20 | void main()
21 | {
22 | fragment = getFragment(uFbo, vTexCo);
23 |
24 | // 'prevViewPosition' is the origin in view space when finding the first hit.
25 | nextFragment = findNextHit(uFbo, fragment, uProjection, vec3(0.0));
26 |
27 | gl_FragColor = nextFragment.color;
28 |
29 | if (!fragment.isValid)
30 | discard;
31 | }
32 |
--------------------------------------------------------------------------------
/demo/shaders/cache-for-deferred-w-tex.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | #pragma glslify: transpose = require('glsl-transpose')
4 | #pragma glslify: inverse = require('glsl-inverse')
5 |
6 | attribute vec2 aTexCo;
7 |
8 | attribute vec3
9 | aPos,
10 | aNormal;
11 |
12 | uniform mat4
13 | uModel,
14 | uProjection,
15 | uView;
16 |
17 | varying vec2 vTexCo;
18 |
19 | varying vec3
20 | vNormal,
21 | vViewPos;
22 |
23 | mat3 normalMatrix;
24 |
25 | mat4 modelViewMatrix;
26 |
27 | vec4 viewPos;
28 |
29 | // TODO (abiro) Move normal matrix computation to CPU.
30 | void main()
31 | {
32 | modelViewMatrix = uView * uModel;
33 |
34 | normalMatrix = transpose(inverse(mat3(modelViewMatrix)));
35 |
36 | vNormal = normalize(normalMatrix * aNormal);
37 |
38 | viewPos = modelViewMatrix * vec4(aPos, 1.0);
39 |
40 | vViewPos = viewPos.xyz;
41 |
42 | vTexCo = aTexCo;
43 |
44 | gl_Position = uProjection * viewPos;
45 | }
--------------------------------------------------------------------------------
/demo/lib/copy/copy.js:
--------------------------------------------------------------------------------
1 | // Copies the contents of a texture to a framebuffer. The texture is assumed
2 | // to be 'gl-texture2d' and the framebuffer 'gl-fbo'. If the latter isn't
3 | // specified, the default framebuffer is used.
4 |
5 | 'use strict'
6 |
7 | var createVAS = require('../../../lib/view-aligned-square.js')
8 | var glslify = require('glslify')
9 | var glShader = require('gl-shader')
10 |
11 | module.exports = function initCopy (gl) {
12 | var geometry = createVAS(gl, 'aPos', 'aTexCoord'),
13 | shader = glShader(
14 | gl,
15 | glslify('./copy.vert'),
16 | glslify('./copy.frag')
17 | )
18 |
19 | return function copy (texture, x, y, w, h, fbo) {
20 | if (fbo) {
21 | fbo.bind()
22 | } else {
23 | gl.bindFramebuffer(gl.FRAMEBUFFER, null)
24 | }
25 |
26 | gl.viewport(x, y, w, h)
27 |
28 | geometry.bind(shader)
29 |
30 | shader.uniforms.uTexture = texture.bind()
31 |
32 | geometry.draw()
33 |
34 | geometry.unbind()
35 | gl.bindFramebuffer(gl.FRAMEBUFFER, null)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/test-case.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var _ = require('underscore')
4 |
5 | module.exports = function testCase () {
6 | var attributes = {}
7 |
8 | if (arguments.length === 0) {
9 | throw new Error('testCase was called without arguments')
10 | }
11 |
12 | _.each(arguments[0], function iteratee (val, key) {
13 | attributes[key] = []
14 | })
15 |
16 | // Underscore treats 'arguments' internally as an array, so in order traversal
17 | // is guaranteed.
18 | _.each(arguments, function iteratee (point) {
19 | _.each(attributes, function iteratee (val, key) {
20 | if (point[key] === undefined) {
21 | throw new Error('Missing key from point: ' + key)
22 | }
23 |
24 | if (point[key].length) {
25 | // flatten
26 | val.push.apply(val, point[key])
27 | } else {
28 | val.push(point[key])
29 | }
30 | })
31 | })
32 |
33 | return {
34 | attributes: attributes,
35 | attributesCount: attributes.isSpecular.length,
36 | points: _.map(arguments, _.identity)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Agost Biro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/lib/get-fragment.glsl:
--------------------------------------------------------------------------------
1 | Fragment getFragment(in FBO fbo, in vec2 texCo)
2 | {
3 | bool
4 | isSpecular,
5 | isValid;
6 |
7 | Fragment invalidFragment;
8 |
9 | vec3
10 | normal,
11 | viewPos;
12 |
13 | vec4
14 | color,
15 | pos;
16 |
17 | // returned to indicate an error.
18 | invalidFragment = Fragment(vec4(0.0), false, false,
19 | vec3(0.0), 0.0, vec3(0.0));
20 |
21 | if (any(lessThan(texCo, vec2(0.0))) || any(greaterThan(texCo, vec2(1.0))))
22 | {
23 | invalidFragment.color = vec4(0.0, 1.0, 0.0, 1.0);
24 |
25 | return invalidFragment;
26 | }
27 |
28 | color = texture2D(fbo.colorSampler, texCo);
29 | isSpecular = texture2D(fbo.isSpecularSampler, texCo).a > 0.0 ? true : false;
30 | normal = normalize(texture2D(fbo.normalSampler, texCo).xyz);
31 | pos = texture2D(fbo.viewPosSampler, texCo);
32 |
33 | viewPos = pos.xyz;
34 | isValid = pos.w == 1.0 ? true : false;
35 |
36 | if (!isValid)
37 | {
38 | invalidFragment.color = vec4(0.0, 1.0, 1.0, 1.0);
39 | return invalidFragment;
40 | }
41 |
42 | return Fragment(color, isSpecular, isValid, normal, 1.0 / pos.z, viewPos);
43 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "screen-space-reflections",
3 | "version": "0.0.1",
4 | "private": true,
5 | "browserify": {
6 | "transform": [
7 | "glslify"
8 | ]
9 | },
10 | "glslify": {
11 | "transform": [
12 | "glslify-import"
13 | ]
14 | },
15 | "scripts": {
16 | "start": "watchify ./demo/index.js -o ./demo/bundle.js --delay 400 -v -d & http-server ./demo -p 8001 -o",
17 | "test": "watchify ./test/test.js -o ./test/bundle.js --delay 400 -v -d & http-server ./test -p 8002 -o"
18 | },
19 | "dependencies": {
20 | "bunny": "^1.0.1",
21 | "canvas-fit": "^1.4.0",
22 | "canvas-orbit-camera": "^1.0.2",
23 | "dat-gui": "^0.5.0",
24 | "gl-buffer": "^2.1.2",
25 | "gl-fbo": "^2.0.5",
26 | "gl-geometry": "^1.1.1",
27 | "gl-mat4": "^1.1.4",
28 | "gl-shader": "^4.0.5",
29 | "gl-texture2d": "^2.0.9",
30 | "gl-vec3": "^1.0.3",
31 | "glsl-diffuse-lambert": "^1.0.0",
32 | "glsl-inverse": "^1.0.0",
33 | "glsl-random": "^0.0.5",
34 | "glsl-specular-blinn-phong": "^1.0.0",
35 | "glsl-transpose": "^1.0.0",
36 | "glslify": "^2.2.1",
37 | "glslify-import": "^1.0.0",
38 | "hex-rgb": "^1.0.0",
39 | "key-pressed": "0.0.1",
40 | "mouse-position": "^1.0.0",
41 | "mouse-pressed": "0.0.1",
42 | "normals": "^1.0.0",
43 | "orbit-camera": "1.0.0",
44 | "scroll-speed": "^1.0.0",
45 | "teapot": "^1.0.0",
46 | "underscore": "^1.8.3",
47 | "vertices-bounding-box": "^1.0.0",
48 | "webgl-context": "^2.2.0"
49 | },
50 | "devDependencies": {
51 | "browserify": "^11.1.0",
52 | "http-server": "^0.8.4",
53 | "grunt": "^0.4.5",
54 | "grunt-browserify": "^4.0.1",
55 | "grunt-contrib-uglify": "^0.9.2",
56 | "grunt-standard": "^1.0.0",
57 | "watchify": "^3.4.0"
58 | }
59 | }
--------------------------------------------------------------------------------
/demo/shaders/deferred-lightning.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | #pragma glslify: blinnPhongSpec = require('glsl-specular-blinn-phong')
4 | #pragma glslify: lambert = require('glsl-diffuse-lambert')
5 |
6 | const vec3
7 | RED = vec3(1.0, 0.0, 0.0),
8 | GREEN = vec3(0.0, 1.0, 0.0);
9 |
10 | uniform sampler2D
11 | uDiffuseColorSampler,
12 | uNormalSampler,
13 | uSpecularColorSampler,
14 | uViewPosSampler;
15 |
16 | uniform vec3
17 | uAmbientLightColor,
18 | uLightPosition;
19 |
20 | varying vec2 vTexCo;
21 |
22 | float
23 | diffusePower,
24 | specularPower,
25 | isValidFragment;
26 |
27 | vec3
28 | lightDirection,
29 | normal,
30 | viewDirection,
31 | viewPos;
32 |
33 | vec4
34 | diffuseColor,
35 | pos,
36 | specularColor;
37 |
38 | void main()
39 | {
40 | diffuseColor = texture2D(uDiffuseColorSampler, vTexCo);
41 | normal = texture2D(uNormalSampler, vTexCo).xyz;
42 | pos = texture2D(uViewPosSampler, vTexCo);
43 | specularColor = texture2D(uSpecularColorSampler, vTexCo);
44 |
45 | viewPos = pos.xyz;
46 | isValidFragment = pos.w;
47 | viewDirection = -1.0 * normalize(viewPos);
48 | lightDirection = normalize(uLightPosition - viewPos);
49 | normal = normalize(normal);
50 |
51 | if (0.0 < diffuseColor.a)
52 | {
53 | diffusePower = lambert(lightDirection, normal);
54 | }
55 | else
56 | {
57 | diffusePower = 0.0;
58 | }
59 |
60 | if (0.0 < specularColor.a)
61 | {
62 | specularPower = blinnPhongSpec(lightDirection, viewDirection,
63 | normal, specularColor.a);
64 | }
65 | else
66 | {
67 | specularPower = 0.0;
68 | }
69 |
70 | gl_FragColor.rgb = uAmbientLightColor +
71 | diffuseColor.rgb * diffusePower +
72 | specularColor.rgb * specularPower;
73 | gl_FragColor.a = 1.0;
74 |
75 | if (isValidFragment != 1.0)
76 | discard;
77 | }
--------------------------------------------------------------------------------
/test/test-point.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var _ = require('underscore')
4 | var mat4 = require('gl-mat4')
5 | var vec3 = require('gl-vec3')
6 |
7 | // TODO (abiro) use check lib
8 | function checkType (value, type) {
9 | if (typeof value !== type)
10 | throw new Error('Expected ' + value + ' to be type ' + type)
11 | }
12 |
13 | function checkVal (value, expected) {
14 | if (value !== expected)
15 | throw new Error('Expected ' + value + ' to be ' + expected)
16 | }
17 |
18 | module.exports = function initTestPoint (config) {
19 | var halfWidth = config.width / 2,
20 | halfHeight = config.height / 2,
21 | projectionMatrix = mat4.create()
22 |
23 | mat4.perspective(
24 | projectionMatrix,
25 | config.fovY,
26 | config.width / config.height,
27 | config.near,
28 | config.far
29 | )
30 |
31 | return function testPoint (position, size, normal, color, isSpecular) {
32 | var clipCoord = vec3.create(),
33 |
34 | ndc,
35 | xw,
36 | yw,
37 | w,
38 | windowCoord
39 |
40 | checkVal(position.length, 3)
41 | checkType(size, 'number')
42 | checkVal(normal.length, 3)
43 | checkVal(color.length, 4)
44 | checkType(isSpecular, 'boolean')
45 |
46 | vec3.transformMat4(clipCoord, position, projectionMatrix)
47 |
48 | w = Math.abs(clipCoord[2])
49 |
50 | ndc = _.map(clipCoord, function iteratee (el) {
51 | return el / w
52 | })
53 |
54 | xw = Math.floor(ndc[0] * halfWidth + halfWidth)
55 | yw = Math.floor(ndc[1] * halfHeight + halfHeight)
56 |
57 | // WebGL seems to map a fragment with NDC.y = 0 below the x axis, but maps
58 | // NDC.x = 0 right to the y axis.
59 | if (halfWidth < xw) {
60 | xw -= 1
61 | }
62 | if (halfHeight <= yw) {
63 | yw -= 1
64 | }
65 |
66 | windowCoord = [xw, yw]
67 |
68 | return {
69 | color: color,
70 | isSpecular: isSpecular ? 1 : 0,
71 | normal: normal,
72 | pos: position,
73 | size: size,
74 | windowCoord: windowCoord
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/screen-space-reflections.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | #pragma glslify: import('./lib/fbo.glsl')
4 | #pragma glslify: import('./lib/fragment.glsl')
5 |
6 | #pragma glslify: import('./lib/coord-transforms.glsl')
7 | #pragma glslify: import('./lib/get-fragment.glsl')
8 | #pragma glslify: import('./lib/march-ray.glsl')
9 |
10 | // TODO (abiro) make this configurable
11 | const int MAX_BOUNCES = 1;
12 |
13 | uniform bool uEmphasizeReflections;
14 |
15 | uniform mat4 uProjection;
16 |
17 | uniform FBO uFbo;
18 |
19 | varying vec2 vTexCo;
20 |
21 | float
22 | cumulativeDistance,
23 | weight;
24 |
25 | Fragment
26 | fragment,
27 | nextFragment;
28 |
29 | vec3
30 | prevViewPosition,
31 | reflectionsColor;
32 |
33 | void main()
34 | {
35 | fragment = getFragment(uFbo, vTexCo);
36 |
37 | // All code is exectued regardless whether it's in a branch or not, so the
38 | // rest of the code will still execute.
39 | if (!fragment.isValid)
40 | discard;
41 |
42 | gl_FragColor = fragment.color;
43 |
44 | // The source of the incident ray is the origin at first.
45 | prevViewPosition = vec3(0.0);
46 | reflectionsColor = vec3(0.0);
47 | cumulativeDistance = 0.0;
48 |
49 | for (int i = 0; i < MAX_BOUNCES; i += 1)
50 | {
51 | if (!fragment.isSpecular)
52 | {
53 | break;
54 | }
55 |
56 | nextFragment = marchRay(uFbo, fragment, uProjection, prevViewPosition);
57 |
58 | if (!nextFragment.isValid)
59 | {
60 | break;
61 | }
62 |
63 | cumulativeDistance += distance(fragment.viewPos, nextFragment.viewPos);
64 |
65 | // The intensity of light is inversely proportional to the square of the
66 | // of the distance from its source.
67 | // TODO (abiro) Need more realistic model for the reflection of different
68 | // materials.
69 | weight = (cumulativeDistance == 0.0) ? 0.0 : 1.0 / cumulativeDistance;
70 |
71 | // TODO (abiro) alpha?
72 | gl_FragColor.rgb = uEmphasizeReflections ?
73 | nextFragment.color.rgb :
74 | gl_FragColor.rgb + nextFragment.color.rgb * weight;
75 |
76 | prevViewPosition = fragment.viewPos;
77 | fragment = nextFragment;
78 | }
79 | }
--------------------------------------------------------------------------------
/test/test-config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var initTestPoint = require('./test-point.js')
4 | var testCase = require('./test-case.js')
5 |
6 | var config = {},
7 |
8 | point
9 |
10 | module.exports = config
11 |
12 | config.clearColor = [0, 0, 0, 0]
13 | config.fovY = Math.PI / 2
14 | config.height = 256
15 | config.far = 100
16 | config.near = 0.01
17 | config.width = 256
18 |
19 | config.rayTrace = {
20 | maxIterations: 3
21 | }
22 |
23 | point = initTestPoint(config)
24 |
25 | var targetSize = 64
26 |
27 | config.testCases = [
28 | testCase(
29 | point([1, 0, -Math.sqrt(3)], 1, [-1, 0, 0], [1, 0, 0, 1], true),
30 | point([0, 0, -2 * Math.sqrt(3)], targetSize, [0, 0, -1], [0, 0, 1, 1], false)
31 | ),
32 | /*testCase(
33 | point([-1, 0, -Math.sqrt(3)], 1, [1, 0, 0], [0, 0, 0, 1], true),
34 | point([0, 0, -2 * Math.sqrt(3)], targetSize, [0, 0, -1], [0, 0, 1, 0], false)
35 | ),
36 | // Case where closer object blocks from view.
37 | testCase(
38 | point([-1, 0, -Math.sqrt(3)], 1, [1, 0, 0], [0, 0, 0, 1], true),
39 | point([0, 0, -2], targetSize, [0, 0, -1], [0, 0, 1, 0], false),
40 | point([0, 0, -2 * Math.sqrt(3)], targetSize, [0, 0, -1], [0, 0, 1, 1], false),
41 | point([0, 0, 1], targetSize, [0, 0, -1], [0, 0, 1, 0], false)
42 | ),
43 | testCase(
44 | point([-1, 0, -Math.sqrt(3)], 1, [1, 0, 0], [0, 0, 0, 1], true),
45 | point([0, 0, -2 * Math.sqrt(3) + 0.01], targetSize, [0, 0, -1], [0, 0, 1, 0], false)
46 | ),*/
47 | testCase(
48 | point([0, 0, -4], 1, [-Math.sqrt(2) / 2, 0, Math.sqrt(2) / 2], [1, 0, 0, 1], true),
49 | point([-1, 0, -4], targetSize, [1, 0, 0], [0, 0, 1, 1], false)
50 | ),
51 | // Ray reflected towards camera
52 | testCase(
53 | point([0, 0, -4], 1, [0, 0, -1], [0, 0, 0, 1], true),
54 | // 0.25 is the code for an invalid fragment facing the camera.
55 | point([0, 0, 1], targetSize, [1, 0, 0], [0.25, 0.25, 0.25, 0.25], false)
56 | ),
57 | /*testCase(
58 | point([0, 0, -1], 64, [0, 0, 0], [1, 0, 0, 1], true),
59 | point([0.5, 0.5, -1], 64, [0, 0, 0], [1, 0, 0, 1], true),
60 | point([-0.5, 0.5, -1], 64, [0, 0, 0], [1, 0, 0, 1], true),
61 | point([0.5, -0.5, -1], 64, [0, 0, 0], [1, 0, 0, 1], true),
62 | point([-0.5, -0.5, -1], 64, [0, 0, 0], [1, 0, 0, 1], true)
63 | )*/
64 | ]
65 |
66 | console.log(config)
67 |
--------------------------------------------------------------------------------
/demo/lib/controls.js:
--------------------------------------------------------------------------------
1 | /*
2 | Original by Hugh Kennedy, MIT License
3 |
4 | https://github.com/hughsk/canvas-orbit-camera
5 |
6 | Original license:
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11 |
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 | */
14 |
15 | // TODO (abiro) Disable roll rotation and looking below surface.
16 |
17 | var createCamera = require('orbit-camera')
18 | var createScroll = require('scroll-speed')
19 | var mp = require('mouse-position')
20 | var mb = require('mouse-pressed')
21 | var key = require('key-pressed')
22 |
23 | module.exports = attachCamera
24 |
25 | function attachCamera (canvas, opts) {
26 | opts = opts || {}
27 | opts.pan = opts.pan !== false
28 | opts.scale = opts.scale !== false
29 | opts.rotate = opts.rotate !== false
30 |
31 | var scroll = createScroll(canvas, opts.scale)
32 | var mbut = mb(canvas, opts.rotate)
33 | var mpos = mp(canvas)
34 | var camera = createCamera(
35 | [60, 60, -60]
36 | , [0, 0, 0]
37 | , [0, 1, 0]
38 | )
39 |
40 | camera.tick = tick
41 |
42 | return camera
43 |
44 | function tick () {
45 | var ctrl = key('') || key('')
46 | var alt = key('')
47 | var height = canvas.height
48 | var width = canvas.width
49 |
50 | if (opts.rotate && mbut.left && !ctrl && !alt) {
51 | camera.rotate(
52 | [ mpos.x / width - 0.5, mpos.y / height - 0.5 ]
53 | , [ mpos.prevX / width - 0.5, mpos.prevY / height - 0.5 ]
54 | )
55 | }
56 |
57 | if (opts.scale && scroll[1]) {
58 | camera.distance *= Math.exp(scroll[1] / height)
59 | }
60 |
61 | if (opts.scale && (mbut.middle || (mbut.left && !ctrl && alt))) {
62 | var d = mpos.y - mpos.prevY
63 | if (!d) return
64 |
65 | camera.distance *= Math.exp(d / height)
66 | }
67 |
68 | camera.distance = Math.min(camera.distance, 80)
69 |
70 | scroll.flush()
71 | mpos.flush()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/test/lib/gl-util.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // Function statements are hoisted.
4 | module.exports = {
5 | createFramebuffer: createFramebuffer,
6 | createTexture: createTexture,
7 | enableAttribute: enableAttribute,
8 | setUniform: setUniform,
9 | setUpShader: setUpShader,
10 | setUpAttribute: setUpAttribute
11 | }
12 |
13 | function createFramebuffer (gl, width, height, colorFilter) {
14 | var fbo = {},
15 |
16 | status
17 |
18 | if (!gl.getExtension('OES_texture_float')) {
19 | throw new Error('OES_texture_float is not supported.')
20 | }
21 |
22 | if (!gl.getExtension('WEBGL_depth_texture')) {
23 | throw new Error('WEBGL_depth_texture is not supported.')
24 | }
25 |
26 | fbo.ref = gl.createFramebuffer()
27 |
28 | fbo.color = createTexture(
29 | gl, null, gl.FLOAT, gl.RGBA,
30 | colorFilter, gl.CLAMP_TO_EDGE, width, height
31 | )
32 |
33 | fbo.depth = createTexture(
34 | gl, null, gl.UNSIGNED_INT,
35 | gl.DEPTH_COMPONENT, gl.NEAREST, gl.CLAMP_TO_EDGE, width, height
36 | )
37 |
38 | gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.ref)
39 |
40 | gl.framebufferTexture2D(
41 | gl.FRAMEBUFFER,
42 | gl.DEPTH_ATTACHMENT,
43 | gl.TEXTURE_2D,
44 | fbo.depth,
45 | 0
46 | )
47 |
48 | gl.framebufferTexture2D(
49 | gl.FRAMEBUFFER,
50 | gl.COLOR_ATTACHMENT0,
51 | gl.TEXTURE_2D,
52 | fbo.color,
53 | 0
54 | )
55 |
56 | // By Florian Boesch
57 | // http://codeflow.org/entries/2013/feb/22/how-to-write-portable-webgl/#how-to-test-if-a-framebuffer-object-is-valid
58 | status = gl.checkFramebufferStatus(gl.FRAMEBUFFER)
59 | switch (status) {
60 | case gl.FRAMEBUFFER_UNSUPPORTED:
61 | throw new Error('Framebuffer is unsupported.')
62 | case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
63 | throw new Error('Framebuffer incomplete attachment.')
64 | case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
65 | throw new Error('Framebuffer incomplete dimensions.')
66 | case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
67 | throw new Error('Framebuffer incomplete missing attachment.')
68 | }
69 | if (status !== gl.FRAMEBUFFER_COMPLETE) {
70 | throw new Error(
71 | 'Framebuffer incomplete for unknown reasons. Status: ' +
72 | status.toString(16)
73 | )
74 | }
75 |
76 | gl.bindFramebuffer(gl.FRAMEBUFFER, null)
77 |
78 | return fbo
79 | }
80 |
81 | function createTexture (gl, pixels, type, format, filter,
82 | wrap, width, height) {
83 | var texture = gl.createTexture()
84 |
85 | gl.activeTexture(gl.TEXTURE0)
86 | gl.bindTexture(gl.TEXTURE_2D, texture)
87 |
88 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
89 |
90 | gl.texImage2D(
91 | gl.TEXTURE_2D, 0, format, width, height, 0, format, type, pixels
92 | )
93 |
94 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter)
95 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter)
96 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap)
97 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap)
98 |
99 | gl.bindTexture(gl.TEXTURE_2D, null)
100 |
101 | return texture
102 | }
103 |
104 | function enableAttribute (gl, program, data) {
105 | gl.useProgram(program)
106 | gl.bindBuffer(data.target, data.buffer)
107 |
108 | gl.vertexAttribPointer(
109 | data.index,
110 | data.itemSize,
111 | data.type,
112 | data.normalized ? true : false,
113 | data.stride ? data.stride : 0,
114 | data.offset ? data.offset : 0
115 | )
116 |
117 | gl.enableVertexAttribArray(data.index)
118 |
119 | gl.bindBuffer(data.target, null)
120 | gl.useProgram(null)
121 | }
122 |
123 | function setUniform (gl, program, data, name) {
124 | if (!data.location) {
125 | if (!name) {
126 | throw new Error('Must provide name or location.')
127 | }
128 |
129 | data.location = gl.getUniformLocation(program, name)
130 | }
131 |
132 | gl.useProgram(program)
133 |
134 | // Matrix uniform setters have a different signature from the others.
135 | if (data.setter.indexOf('Matrix') < 0) {
136 | gl[data.setter](data.location, data.value)
137 | } else {
138 | gl[data.setter](data.location, data.transpose, data.value)
139 | }
140 |
141 | gl.useProgram(null)
142 | }
143 |
144 | function setUpShader (gl, program, shader, source) {
145 | gl.shaderSource(shader, source)
146 | gl.compileShader(shader)
147 |
148 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
149 | console.log(gl.getShaderInfoLog(shader))
150 | throw new Error('Shader failed to compile.')
151 | }
152 |
153 | gl.attachShader(program, shader)
154 | }
155 |
156 | function setUpAttribute (gl, program, name, data) {
157 | data.index = gl.getAttribLocation(program, name)
158 | data.buffer = gl.createBuffer()
159 | data.numItems = data.value.length / data.itemSize
160 |
161 | gl.useProgram(program)
162 | gl.bindBuffer(data.target, data.buffer)
163 | gl.bufferData(data.target, data.value, data.usage)
164 | gl.useProgram(null)
165 | }
166 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var _ = require('underscore')
4 | var config = require('./test-config.js')
5 | var createBuffer = require('gl-buffer')
6 | var createFbo = require('gl-fbo')
7 | var createViewAlignedSquare = require('../lib/view-aligned-square.js')
8 | var glShader = require('gl-shader')
9 | var glslify = require('glslify')
10 | var mat4 = require('gl-mat4')
11 |
12 | function elementsEqual (array1, array2) {
13 | return array1.length === array2.length &&
14 | _.every(_.zip(array1, array2), function predicate (el) {
15 | // Avoid divison by 0.
16 | el[0] += 1
17 | el[1] += 1
18 |
19 | // Floating point numbers aren't precise.
20 | return Math.abs(el[0] / el[1] - 1) < 0.001
21 | })
22 | }
23 |
24 | var canvas = document.createElement('canvas'),
25 | gl = canvas.getContext('webgl', {antialias: false}),
26 |
27 | deferredShadingFbo = createFbo(
28 | gl,
29 | [config.width, config.height],
30 | {
31 | float: true,
32 | color: 4
33 | }
34 | ),
35 | // fboOut = createFbo(gl, [config.width, config.height]),
36 |
37 | viewAlignedSquareGeo = createViewAlignedSquare(gl, 'aPos', 'aTexCo'),
38 |
39 | cacheForDeferredShader = glShader(
40 | gl,
41 | glslify('./shaders/cache-for-deferred.vert'),
42 | glslify('./shaders/cache-for-deferred.frag')
43 | ),
44 | testRayTraceShader = glShader(
45 | gl,
46 | glslify('./shaders/test-ray-trace.vert'),
47 | glslify('./shaders/test-ray-trace.frag')
48 | ),
49 | /*testShader = glShader(
50 | gl,
51 | glslify('./shaders/test.vert'),
52 | glslify('./shaders/test.frag')
53 | ),*/
54 |
55 | projectionMatrix = mat4.create(),
56 |
57 | pass = true
58 |
59 | canvas.width = config.width
60 | canvas.height = config.height
61 |
62 | if (gl.drawingBufferWidth !== config.width ||
63 | gl.drawingBufferHeight !== config.height) {
64 | throw new Error('Drawing buffer size mismatch.')
65 | }
66 |
67 | mat4.perspective(
68 | projectionMatrix,
69 | config.fovY,
70 | config.width / config.height,
71 | config.near,
72 | config.far
73 | )
74 |
75 | gl.clearColor.apply(gl, config.clearColor)
76 | gl.enable(gl.DEPTH_TEST)
77 |
78 | // TODO (abiro) Figure out why 'gl-geometry' messes up attributes when it is
79 | // used for rendering points. (Using buffers directly instead now as a
80 | // workaround.)
81 | // TODO (abiro) Fix 'readPixels' on Firefox.
82 | config.testCases.forEach(function iteratee (testCase, i) {
83 | var pixel = new Uint8Array(4),
84 |
85 | colorBuffer,
86 | normalsBuffer,
87 | pointSizeBuffer,
88 | positionsBuffer,
89 | isSpecularBuffer,
90 | firstPoint,
91 | lastPoint,
92 | normalizedPixel
93 |
94 | colorBuffer = createBuffer(gl, testCase.attributes.color)
95 | normalsBuffer = createBuffer(gl, testCase.attributes.normal)
96 | pointSizeBuffer = createBuffer(gl, testCase.attributes.size)
97 | positionsBuffer = createBuffer(gl, testCase.attributes.pos)
98 | isSpecularBuffer = createBuffer(gl, testCase.attributes.isSpecular)
99 |
100 | deferredShadingFbo.bind()
101 |
102 | cacheForDeferredShader.bind()
103 |
104 | colorBuffer.bind()
105 | cacheForDeferredShader.attributes.aColor.pointer()
106 |
107 | normalsBuffer.bind()
108 | cacheForDeferredShader.attributes.aNormal.pointer()
109 |
110 | pointSizeBuffer.bind()
111 | cacheForDeferredShader.attributes.aPointSize.pointer()
112 |
113 | positionsBuffer.bind()
114 | cacheForDeferredShader.attributes.aPos.pointer()
115 |
116 | isSpecularBuffer.bind()
117 | cacheForDeferredShader.attributes.aIsSpecular.pointer()
118 |
119 | gl.bindBuffer(gl.ARRAY_BUFFER, null)
120 |
121 | cacheForDeferredShader.uniforms.uProjection = projectionMatrix
122 |
123 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
124 | gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
125 | gl.drawArrays(gl.POINTS, 0, testCase.attributesCount)
126 |
127 | // fboOut.bind()
128 | gl.bindFramebuffer(gl.FRAMEBUFFER, null)
129 | viewAlignedSquareGeo.bind(testRayTraceShader)
130 | testRayTraceShader.uniforms.uProjection = projectionMatrix
131 | testRayTraceShader.uniforms.uFbo = {
132 | colorSampler: deferredShadingFbo.color[0].bind(0),
133 | viewPosSampler: deferredShadingFbo.color[1].bind(1),
134 | normalSampler: deferredShadingFbo.color[2].bind(2),
135 | size: [gl.drawingBufferWidth, gl.drawingBufferHeight],
136 | isSpecularSampler: deferredShadingFbo.color[3].bind(3)
137 | }
138 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
139 | gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
140 | viewAlignedSquareGeo.draw()
141 | viewAlignedSquareGeo.unbind()
142 |
143 | /*gl.bindFramebuffer(gl.FRAMEBUFFER, null)
144 | viewAlignedSquareGeo.bind(testShader)
145 | testShader.uniforms.uProjection = projectionMatrix
146 | testShader.uniforms.uFbo = {
147 | colorSampler: deferredShadingFbo.color[0].bind(0),
148 | viewPosSampler: deferredShadingFbo.color[1].bind(1),
149 | normalSampler: deferredShadingFbo.color[2].bind(2),
150 | isSpecularSampler: deferredShadingFbo.color[3].bind(3)
151 | }
152 | gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
153 | viewAlignedSquareGeo.draw()
154 | viewAlignedSquareGeo.unbind();*/
155 |
156 | firstPoint = _.first(testCase.points)
157 | lastPoint = _.last(testCase.points)
158 |
159 | gl.readPixels(
160 | firstPoint.windowCoord[0], firstPoint.windowCoord[1], 1, 1,
161 | gl.RGBA, gl.UNSIGNED_BYTE, pixel
162 | )
163 |
164 | normalizedPixel = _.map(pixel, function iteratee (el) {
165 | return el / 255
166 | })
167 |
168 | /*_.each(_.range(config.width), function iteratee(w)
169 | {
170 | _.each(_.range(config.height), function iteratee(h)
171 | {
172 | gl.readPixels(
173 | w, h, 1, 1,
174 | gl.RGBA, gl.UNSIGNED_BYTE, pixel
175 | )
176 | console.log(w, h, pixel)
177 | })
178 | });*/
179 |
180 | if (!elementsEqual(normalizedPixel, lastPoint.color)) {
181 | console.error('Test case ' + (i + 1) + ' has failed.')
182 | console.error(
183 | firstPoint.windowCoord[0], firstPoint.windowCoord[1],
184 | normalizedPixel, firstPoint.color, lastPoint.color
185 | )
186 |
187 | pass = false
188 | }
189 | })
190 |
191 | canvas.style.border = 'solid 1px'
192 | document.body.appendChild(canvas)
193 | // document.body.style.background = pass ? 'green' : 'red'
194 |
--------------------------------------------------------------------------------
/src/lib/march-ray.glsl:
--------------------------------------------------------------------------------
1 | #pragma glslify: random = require('glsl-random')
2 |
3 | Fragment marchRay(in FBO fbo,
4 | in Fragment fragment,
5 | in mat4 projectionMatrix,
6 | in vec3 prevViewPosition)
7 | {
8 | // TODO (abiro) Make it configurable.
9 | const float
10 | MAX_ITERATIONS = 20.0,
11 | MAX_STRIDE = 16.0,
12 | SEARCH_STEPS = 3.0;
13 |
14 | const vec3
15 | CAMERA_DIR = vec3(0.0, 0.0, 1.0),
16 | DOWN_DIR = vec3(0.0, -1.0, 0.0);
17 |
18 | bool
19 | canReflect,
20 | coarseHit,
21 | isDownFacing,
22 | sameDir;
23 |
24 | float
25 | dotProductNormalReflRay,
26 | dotProductNormalNextReflRay,
27 | nextPosReciprocalZ,
28 | targetPosReciprocalZ,
29 | startingPosReciprocalZ,
30 | steps,
31 | stepRatio,
32 | stepsBack;
33 |
34 | Fragment
35 | invalidFragment,
36 | nextFragment,
37 | prevFragment;
38 |
39 | vec2
40 | nextPos,
41 | searchDirection,
42 | searchDirUnitLen,
43 | startingPos,
44 | targetPos;
45 |
46 | vec3
47 | incidentRay,
48 | reflectedRay;
49 |
50 | vec4
51 | fragmentClipCoord,
52 | targetClipCoord;
53 |
54 | // Returned to indicate no hit.
55 | invalidFragment = Fragment(vec4(0.0), false, false, vec3(0.0),
56 | 0.0, vec3(0.0));
57 |
58 | incidentRay = normalize(fragment.viewPos - prevViewPosition);
59 | reflectedRay = reflect(incidentRay, fragment.normal);
60 |
61 | // Reflections toward the camera would fail on the depth test and cause other
62 | // complications (hit out of frame objects or encounter occluders).
63 | if (dot(reflectedRay, CAMERA_DIR) > 0.0)
64 | {
65 | invalidFragment.color = vec4(1.0, 0.0, 1.0, 1.0);
66 | return invalidFragment;
67 | }
68 |
69 | targetClipCoord = projectionMatrix * vec4(fragment.viewPos + reflectedRay, 1.0);
70 | fragmentClipCoord = projectionMatrix * vec4(fragment.viewPos, 1.0);
71 |
72 | dotProductNormalReflRay = dot(fragment.normal, reflectedRay);
73 |
74 | isDownFacing = dot(fragment.normal, DOWN_DIR) > 0.0;
75 |
76 | // Screen space is a space here where integer coordinates correspond to pixels
77 | // with origin in the middle.
78 | // TODO (abiro) to which part of a pixel does the integer coordinate
79 | // correspond to?
80 | startingPos = toScreenSpaceCoord(fragmentClipCoord.xy / fragmentClipCoord.w,
81 | fbo.size);
82 |
83 | // Using reciprocals allows linear interpolation in screen space.
84 | // Making depth comparisons in clip space to avoid having to convert to
85 | // view space from clip.
86 | startingPosReciprocalZ = 1.0 / fragmentClipCoord.z;
87 |
88 | targetPos = toScreenSpaceCoord(targetClipCoord.xy / targetClipCoord.w,
89 | fbo.size);
90 |
91 | targetPosReciprocalZ = 1.0 / targetClipCoord.z;
92 |
93 | searchDirection = targetPos - startingPos;
94 |
95 | // TODO (abiro) This doesn't guarantee that all pixels are visited along the
96 | // path. Use DDA.
97 | searchDirUnitLen = normalize(searchDirection);
98 |
99 | stepRatio = 1.0 / length(searchDirection);
100 |
101 | nextPos = vec2(0.0);
102 |
103 | for (float i = 1.0; i <= MAX_ITERATIONS; i += 1.0)
104 | {
105 | steps = i * MAX_STRIDE * random(nextPos);
106 |
107 | // Find the next position in screen space to test for a hit along the
108 | // reflected ray.
109 | nextPos = startingPos + steps * searchDirUnitLen;
110 |
111 | // Find the z value at the next position to test in view space.
112 | nextPosReciprocalZ = mix(startingPosReciprocalZ,
113 | targetPosReciprocalZ,
114 | steps * stepRatio);
115 |
116 | // Get the fragment from the framebuffer that could be reflected to the
117 | // current fragment and see if it is actually reflected there.
118 | nextFragment = getFragment(fbo, screenSpaceToTexco(nextPos, fbo.size));
119 |
120 | // TODO (abiro) rethink this
121 | if (!nextFragment.isValid)
122 | {
123 | return nextFragment;
124 | }
125 |
126 | // If the dot product of two vectors is negative, they are facing entirely
127 | // different directions.
128 | canReflect = dot(reflectedRay, nextFragment.normal) < 0.0;
129 |
130 | // See if the point is along the ray.
131 | // See commit message d2897ae on down-facing objects.
132 | sameDir = (isDownFacing ? 0.5 : 0.99) <
133 | dot(reflectedRay,
134 | normalize(nextFragment.viewPos - fragment.viewPos));
135 |
136 | // TODO (abiro) Rethink this. 'nextPosReciprocalZ' is in clip space, while
137 | // 'nextFragment.reciprocalZ' is in view space. This should have not matter,
138 | // as long as ray is within the image.
139 |
140 | // Test if the ray is behind the object.
141 | // Z values are negative in clip space, but the reciprocal switches
142 | // relations.
143 | if (nextPosReciprocalZ >= nextFragment.reciprocalZ && sameDir && canReflect)
144 | {
145 | coarseHit = true;
146 | break;
147 | }
148 | // In this case, a closer object is blocking a possible hit from view.
149 | else if (sameDir && canReflect)
150 | {
151 | invalidFragment.color = vec4(0.0, 0.0, 1.0, 1.0);
152 | return invalidFragment;
153 | }
154 | }
155 |
156 | if (!coarseHit)
157 | {
158 | invalidFragment.color = vec4(1.0, 0.0, 0.0, 1.0);
159 | return invalidFragment;
160 | }
161 |
162 | if (!nextFragment.isValid)
163 | {
164 | invalidFragment.color = vec4(1.0, 1.0, 0.0, 1.0);
165 | return invalidFragment;
166 | }
167 |
168 | // Refine the match by binary search. If the ray is behind the current
169 | // fragment's position, take a half-stride long step back and reexamine
170 | // the depth buffer. Then, taking smaller and smaller steps recursively
171 | // find the closest hit for the ray.
172 | for (float j = SEARCH_STEPS; j >= 0.0; j -= 1.0)
173 | {
174 | dotProductNormalNextReflRay = dot(
175 | fragment.normal,
176 | normalize(nextFragment.viewPos - fragment.viewPos)
177 | );
178 |
179 | if (dotProductNormalReflRay < dotProductNormalNextReflRay)
180 | {
181 | // Step back
182 | stepsBack = -pow(2.0, j);
183 | }
184 | else if (dotProductNormalReflRay > dotProductNormalNextReflRay)
185 | {
186 | // Step forward
187 | stepsBack = pow(2.0, j);
188 | }
189 | else
190 | {
191 | break;
192 | }
193 |
194 | steps = steps + stepsBack;
195 |
196 | nextPos = startingPos + steps * searchDirUnitLen;
197 | prevFragment = nextFragment;
198 | nextFragment = getFragment(fbo, screenSpaceToTexco(nextPos, fbo.size));
199 |
200 | // TODO (abiro) rethink this
201 | if (!nextFragment.isValid)
202 | {
203 | nextFragment = prevFragment;
204 | break;
205 | }
206 | }
207 |
208 | return nextFragment;
209 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Report on Screen-Space Reflections
2 | *Agost Biro, 2015-10-02*
3 |
4 | ## Intro
5 |
6 | Screen-space reflections (SSR) are a deferred rendering method for realistic reflections. The advantage of SSR over reflection maps and planar reflections is that it can calculate reflections for dynamic changes in the scene (e.g. the reflections of an avatar or NPCs) and for all reflecting objects. The disadvantage is that SSR can only render reflections of objects visible in the frame and it is more demanding in terms of performance than reflection maps. The performance difference between SSR and planar reflections is unclear.
7 |
8 | SSR works by taking a shaded frame in a framebuffer object with multiple buffers and then performs ray tracing for each fragment to find other fragments that might be reflected in that fragment. The output is the color contribution of the other, reflected fragments to the current fragment.
9 |
10 | See live demo here: http://abiro.github.io/screen-space-reflections/
11 |
12 | ## Ray tracing and ray marching
13 |
14 | Ray tracing is a sequence of recursive ray marching steps. Ray marching is a procedure where a ray is iteratively advanced through some space to determine whether that ray hits an object in that space. In the case of SSR, ray marching is performed in screen space, hence the name. Screen space is a 2D space corresponding to the image plane. I found it most practical to set units so that integers correspond to pixels and the origin is at the center (I'm assuming symmetric FOV).
15 |
16 | While the ray is advanced in screen space, collision detection is performed in view or clip space. The problem of collision detection is reduced here to the problem of deciding whether a point lies on a line segment. This can be approximated by testing whether the depth of the ray at an iteration is behind the depth of the fragment that is at a coordinate in screen space where a point along the ray’s path would be projected to. Depth testing is performed in my implementation in clip space using the reciprocal of z-coordinates. The reciprocals of z-coordinates are used, because this way linear interpolation in screen space produces perspective-correct values.
17 |
18 | ## Hit testing
19 |
20 | The desktop implementations I've looked at [1, 5] seem to perform only a depth test to perform collision detection. My experience found this approach lacking. Consider the case of a planar surface parallel to the image plane. Using only depth testing, a hit will be produced for the bottom of the surface for positions that should reflect the top, as ray marching will encounter fragments at the bottom first and those will satisfy the depth test as points on the surface share the same depth. To avoid such false positives, I chose to add an additional test for ray direction. The dot product of the reflected ray and the direction between the reflector and reflected fragment should be close to 1 to pass. This is a relatively cheap test to perform, but greatly increases image quality. I also test whether the candidate surface actually faces the ray to avoid some artifacts caused by reflecting the wrong side of an object.
21 |
22 | Marching a ray pixel-by-pixel through a frame to detect reflections is not feasible for performance reasons. The solution is to take large strides across the screen space until a coarse hit is found and then recursively refine the match with a binary search-like procedure. The literature [1] proposes using depth as criteria for the refinement, but I've found it better to converge on the reflected ray's direction. The problem with using depth as criteria for refinement, in addition to the aforementioned parallel planar surface problem, is that a lot of precision is lost due to comparing reciprocal z-values.
23 |
24 | My approach rests on the assumption that depth on the surfaces of objects does not vary much over a stride, which I think is a valid assumption for strides of, say, 16 pixels in closed environments. If there was a large depth variance over such a small area, then the object would have to be pretty far away, but in that case a reflection map is a better choice than screen-space reflections anyway. The drawback is that the method breaks if the stride happens to cover two or more objects with large depth differences.
25 |
26 | ## Realistic reflections
27 |
28 | Apart from ray tracing, an other issue to consider with SSR is the question of reflectance, refraction and attenuation. A robust implementation must take into account the material of surfaces to render more realistic reflections. By opting for glossy surfaces, this can also help with masking artifacts.
29 |
30 | ## Edge cases
31 |
32 | There are numerous edge cases where SSR breaks that all implementations must carefully consider. Most of these arise from the discrete nature of SSR, meaning that SSR can only work with surfaces visible in the frame.
33 |
34 | Surfaces may not be visible in a frame, because they are occluded by other objects, they are facing away from the camera, or because they are simply outside the frame. These issues are encountered in particular when objects reflect rays towards the camera. Supporting rays reflected towards the camera also needs adjustments to depth testing, as the ray will travel to opposite direction.
35 |
36 | The bottom part of convex objects that lie on the ground pose a challenge, because the ray march is liable to step over their small area of possible hits without producing a coarse hit. A different challenge is that the bottom part will be occluded in the frame, yet its reflection should be visible on the floor.
37 |
38 | ## Demo
39 |
40 | The [demo](http://abiro.github.io/screen-space-reflections/) targets Chrome browsers and the scene consists of a specular, textured, tiled floor, a highly specular Utah teapot and 3 diffusely shaded Stanford bunnies. Other than ambient lightning, there is a single directional light source in the scene. Shadows are not implemented.
41 |
42 | The current state of the demo (2015-10-02) implements SSR with only 1 ray tracing step and performs ray marching with 16 pixels strides refined by a 4 step binary search at the end. To produce a coarse hit, the ray's depth must be higher than the candidate fragments, the direction between the candidate's view position and the original position must closely match the reflected ray's direction and the surface of the candidate fragment must face the ray. The binary search-like refinement procedure converges on the direction of the candidate ray and the reflected ray.
43 |
44 | All reflecting surfaces are considered to be perfect mirrors in the demo and the reflected colors are simply divided by the magnitude of the reflected ray. (Following the inverse square law, one should divide by the square of the distance, but I wanted to emphasize reflections a little more.) A robust implementation should account for the material of the reflected objects.
45 |
46 | Staircase artifacts are present in the demo. I've tried to eliminate these by implementing jittering, but it only resulted in minimal improvements, so I discarded it. As it would be advisable to implement SSR at a lower resolution than the window's for performance, the staircase artifacts could be smoothed over during upsampling.
47 |
48 | Different artifacts are present in the demo due to missing information in the frame. This is apparent in the objects' reflections in the floor where a slice is missing near the bottom at some angles. This is due to the bottom of the objects being occluded by their higher parts. Another case is the incomplete reflections of the bunnies in the teapot, where the parts that should be reflected are either outside the frame, face a different direction or the rays toward them are killed on account of moving towards the camera.
49 |
50 | Artifacts at the bottom half of the teapot are present due to the ray march stepping over the small area of possible hits without producing a coarse hit. If the stride is set to 1 these artifacts completely disappear, but that is not feasible for performance. I've experimented with progressively increasing the stride in the beginning, but that didn't work either, as the area to cover is too large. In the end, I managed to greatly reduce the severity of this artifact by increasing the tolerance of direction matching for down-facing surfaces to allow for coarse matches that can then be refined by binary search. The assumption here is that down-facing surfaces are close to the ground and a higher variance in angles is tolerable for short distances.
51 |
52 | Finally, the current implementation of SSR assumes a square frame for simplicity. To work with other aspect ratios, a rasterization algorithm such as DDA must be employed.
53 |
54 | ## Future work
55 |
56 | ### Short term
57 | - See if upsampling or a blur gets rid of the staircase artifacts
58 | - Allow changing stride and max iterations in the widget
59 | - Proper testing framework for the ray tracing procedure
60 | - An easy-to-use JS-module to allow for testing on scenes other than the demo
61 |
62 | ### Long term
63 | - Fine-tune performance
64 | - Account for material in reflection
65 | - DDA to make sure every pixel is visited
66 | - Antialiasing
67 | - Use advanced data structure to speed up ray marching (see [2-4])
68 | - Reprojections from other viewpoints to provide off-screen reflections
69 |
70 | ## Conclusion
71 |
72 | The project in its current form shows the potential of screen-space reflections and provides a solid foundation for developing a robust, production-ready implementation.
73 |
74 | ## Sources
75 | 1. McGuire, Morgan and Michael Mara. "Efficient GPU Screen-Space Ray Tracing." Journal of Computer Graphics Techniques (JCGT). Vol. 3. No. 4. 2014.
76 | 1. Uludag, Yasin. "Hi-Z Screen-Space Cone-Traced Reflections." GPU Pro 5: Advanced Rendering Techniques (2014): 149.
77 | 1. Widmer, S., et al. "An adaptive acceleration structure for screen-space ray tracing." Proceedings of the 7th Conference on High-Performance Graphics. ACM, 2015.
78 | 1. Yu, Xuan, Rui Wang, and Jingyi Yu. "Interactive Glossy Reflections using GPU‐based Ray Tracing with Adaptive LOD." Computer Graphics Forum. Vol. 27. No. 7. Blackwell Publishing Ltd, 2008.
79 | 1. "Screen Space Reflections in Unity 5." kode80. kode80, 11 March 2015. Web. 2 October 2015.
80 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | // TODO (abiro) standard style
2 | // TODO (abiro) magic numbers
3 |
4 | 'use strict'
5 |
6 | // Browserify can't handle comma-separated requires.
7 | var boundingBox = require('vertices-bounding-box')
8 | var bunny = require('bunny')
9 | var initCopy = require('./lib/copy/copy.js')
10 | var createCanvasOrbitCamera = require('./lib/controls.js')
11 | var createFBO = require('gl-fbo')
12 | var createTexture = require('gl-texture2d')
13 | var createViewAlignedSquare = require('../lib/view-aligned-square.js')
14 | var datGui = require('dat-gui')
15 | var floor = require('./lib/floor.js')
16 | var FPSMeter = require('./lib/fpsmeter.js')
17 | var getContext = require('webgl-context')
18 | var glGeometry = require('gl-geometry')
19 | var glShader = require('gl-shader')
20 | var glslify = require('glslify')
21 | var hexRgb = require('hex-rgb')
22 | var normals = require('normals')
23 | var opts = require('./options.js')
24 | var mat4 = require('gl-mat4')
25 | var teapot = require('teapot')
26 | var vec3 = require('gl-vec3')
27 |
28 | window.onload = function onload () {
29 | var canvas = document.getElementById('gl-canvas'),
30 | gl = getContext({canvas: canvas}),
31 | WEBGL_draw_buffers_extension = gl.getExtension('WEBGL_draw_buffers'),
32 | OES_texture_float_extension = gl.getExtension('OES_texture_float'),
33 |
34 | copy = initCopy(gl),
35 |
36 | gui = new datGui.GUI(),
37 | guiFolders = {},
38 |
39 | meter = new FPSMeter({theme: 'transparent', show: 'ms'}),
40 |
41 | camera = createCanvasOrbitCamera(canvas, {pan: false}),
42 |
43 | // A simple directional light.
44 | lightViewPosition = vec3.create(),
45 |
46 | bunnyGeo = glGeometry(gl),
47 | floorGeo = glGeometry(gl),
48 | teapotGeo = glGeometry(gl),
49 | viewAlignedSquareGeo = createViewAlignedSquare(gl, 'aPos', 'aTexCo'),
50 |
51 | bunnyDiffuseColor = [0.78, 0.41, 0.29],
52 | floorTexture = createTexture(gl, document.getElementById('floor-texture')),
53 |
54 | bunnyShader = glShader(
55 | gl,
56 | glslify('./shaders/cache-for-deferred.vert'),
57 | glslify('./shaders/cache-for-deferred.frag')
58 | ),
59 | deferredLightningShader = glShader(
60 | gl,
61 | glslify('./shaders/deferred-lightning.vert'),
62 | glslify('./shaders/deferred-lightning.frag')
63 | ),
64 | floorShader = glShader(
65 | gl,
66 | glslify('./shaders/cache-for-deferred-w-tex.vert'),
67 | glslify('./shaders/cache-for-deferred-w-tex.frag')
68 | ),
69 | screenSpaceReflectionsShader = glShader(
70 | gl,
71 | glslify('../src/screen-space-reflections.vert'),
72 | glslify('../src/screen-space-reflections.frag')
73 | ),
74 | teapotShader = glShader(
75 | gl,
76 | glslify('./shaders/cache-for-deferred.vert'),
77 | glslify('./shaders/cache-for-deferred.frag')
78 | ),
79 |
80 | modelMatrix = mat4.create(),
81 | bunnyModelMatrix = mat4.create(),
82 | floorModelMatrix = mat4.create(),
83 | teapotModelMatrix = mat4.create(),
84 |
85 | projectionMatrix = mat4.create(),
86 | viewMatrix = mat4.create(),
87 |
88 | bunnyPositions = [
89 | [-10, 0, 0],
90 | [10, 0, 0],
91 | [0, 0, -15]
92 | ],
93 | bunnyRotations = [Math.PI / 2, -Math.PI / 2, 0],
94 |
95 | bunnyBoundingBox,
96 | bufferSize,
97 | displaySize,
98 | deferredShadingFbo,
99 | devicePixelRatio,
100 | displayDim,
101 | displayScaledSize,
102 | firstPassFbo,
103 | outFbo,
104 | size,
105 | teapotBoundingBox
106 |
107 | function hexRGBNormalize (hex) {
108 | return hexRgb(hex).map(function iteratee (el) {
109 | return el / 255
110 | })
111 | }
112 |
113 | function drawObjects () {
114 | var dls,
115 | ssr
116 |
117 | meter.tick()
118 |
119 | camera.view(viewMatrix)
120 | camera.tick()
121 |
122 | gl.viewport(0, 0, bufferSize[0], bufferSize[1])
123 |
124 | mat4.perspective(
125 | projectionMatrix,
126 | Math.PI / 4,
127 | bufferSize[0] / bufferSize[1],
128 | 1,
129 | 300
130 | )
131 |
132 | vec3.transformMat4(
133 | lightViewPosition,
134 | [opts.lights.posX, opts.lights.posY, opts.lights.posZ],
135 | viewMatrix
136 | )
137 |
138 | deferredShadingFbo.bind()
139 |
140 | gl.clearColor(0, 0, 0, 0)
141 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
142 |
143 | bunnyGeo.bind(bunnyShader)
144 | bunnyShader.uniforms.uDiffuseColor = bunnyDiffuseColor
145 | bunnyShader.uniforms.uView = viewMatrix
146 | bunnyShader.uniforms.uProjection = projectionMatrix
147 | bunnyShader.uniforms.uShininess = 0
148 | bunnyShader.uniforms.uUseDiffuseLightning = 1
149 |
150 | mat4.identity(modelMatrix)
151 | bunnyPositions.forEach(function iteratee (pos, i) {
152 | mat4.translate(modelMatrix, bunnyModelMatrix, pos)
153 |
154 | mat4.rotateY(modelMatrix, modelMatrix, bunnyRotations[i])
155 |
156 | bunnyShader.uniforms.uModel = modelMatrix
157 |
158 | bunnyGeo.draw()
159 | })
160 |
161 | bunnyGeo.unbind()
162 |
163 | floorGeo.bind(floorShader)
164 | floorShader.uniforms.uModel = floorModelMatrix
165 | floorShader.uniforms.uView = viewMatrix
166 | floorShader.uniforms.uProjection = projectionMatrix
167 | floorShader.uniforms.uShininess = opts.floor.shininess,
168 | floorShader.uniforms.uSpecularColor = hexRGBNormalize(opts.floor.specularColor),
169 | floorShader.uniforms.uTexture = floorTexture.bind()
170 | floorShader.uniforms.uUseDiffuseLightning = 1
171 | floorGeo.draw()
172 | floorGeo.unbind()
173 |
174 | teapotGeo.bind(teapotShader)
175 | teapotShader.uniforms.uModel = teapotModelMatrix
176 | teapotShader.uniforms.uView = viewMatrix
177 | teapotShader.uniforms.uProjection = projectionMatrix
178 | teapotShader.uniforms.uShininess = opts.teapot.shininess,
179 | teapotShader.uniforms.uSpecularColor = hexRGBNormalize(opts.teapot.specularColor),
180 | teapotShader.uniforms.uUseDiffuseLightning = 0
181 | teapotGeo.draw()
182 | teapotGeo.unbind()
183 |
184 | if (opts.reflectionsOn) {
185 | firstPassFbo.bind()
186 | } else {
187 | outFbo.bind()
188 | }
189 | gl.clearColor(0.9, 0.95, 1, 1)
190 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
191 |
192 | // TODO (abiro) antialiasing
193 | dls = deferredLightningShader
194 | viewAlignedSquareGeo.bind(dls)
195 | dls.uniforms.uAmbientLightColor = hexRGBNormalize(opts.lights.ambientColor)
196 | dls.uniforms.uLightPosition = lightViewPosition
197 | dls.uniforms.uViewPosSampler = deferredShadingFbo.color[0].bind(0)
198 | dls.uniforms.uNormalSampler = deferredShadingFbo.color[1].bind(1)
199 | dls.uniforms.uDiffuseColorSampler = deferredShadingFbo.color[2].bind(2)
200 | dls.uniforms.uSpecularColorSampler = deferredShadingFbo.color[3].bind(3)
201 | viewAlignedSquareGeo.draw()
202 | viewAlignedSquareGeo.unbind()
203 |
204 | if (opts.reflectionsOn) {
205 | outFbo.bind()
206 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
207 |
208 | ssr = screenSpaceReflectionsShader
209 | viewAlignedSquareGeo.bind(ssr)
210 | ssr.uniforms.uEmphasizeReflections = opts.emphasizeReflections
211 | ssr.uniforms.uFbo = {
212 | size: bufferSize,
213 | viewPosSampler: deferredShadingFbo.color[0].bind(0),
214 | normalSampler: deferredShadingFbo.color[1].bind(1),
215 | colorSampler: firstPassFbo.color[0].bind(2),
216 | isSpecularSampler: deferredShadingFbo.color[3].bind(3)
217 | }
218 | ssr.uniforms.uProjection = projectionMatrix
219 | viewAlignedSquareGeo.draw()
220 | viewAlignedSquareGeo.unbind()
221 | }
222 |
223 | outFbo.color[0].magFilter = gl.LINEAR
224 | copy(outFbo.color[0], 0, 0, displayScaledSize[0], displayScaledSize[1])
225 |
226 | window.requestAnimationFrame(drawObjects)
227 | }
228 |
229 | if (!WEBGL_draw_buffers_extension) {
230 | throw new Error('The WEBGL_draw_buffers extension is unavailable.')
231 | }
232 |
233 | if (!OES_texture_float_extension) {
234 | throw new Error('The OES_texture_float extension is unavailable.')
235 | }
236 |
237 | devicePixelRatio = window.devicePixelRatio || 1;
238 | size = 512;
239 | bufferSize = [size, size];
240 | displayDim = Math.max(
241 | Math.min(window.innerWidth, window.innerHeight),
242 | size
243 | );
244 | displaySize = [displayDim, displayDim];
245 | displayDim *= devicePixelRatio
246 | displayScaledSize = [displayDim, displayDim];
247 |
248 | canvas.width = displayScaledSize[0]
249 | canvas.height = displayScaledSize[1]
250 |
251 | canvas.style.width = displaySize[0] + 'px'
252 | canvas.style.height = displaySize[1] + 'px'
253 | canvas.style.border = 'solid 1px'
254 |
255 | // Color buffers are eye-space position, eye-space normal, diffuse color
256 | // and specular color, respectively. A value larger than 0 in the alpha
257 | // channels of the diffuse and specular colors means the appropriate
258 | // lightning model is used.
259 | deferredShadingFbo = createFBO(
260 | gl,
261 | bufferSize,
262 | {
263 | float: true,
264 | color: 4
265 | }
266 | );
267 | firstPassFbo = createFBO(gl, bufferSize);
268 | outFbo = createFBO(gl, bufferSize);
269 |
270 | gui.add(opts, 'emphasizeReflections')
271 | gui.add(opts, 'reflectionsOn')
272 |
273 | guiFolders.floor = gui.addFolder('Floor')
274 | guiFolders.floor.add(opts.floor, 'shininess', 1, 50)
275 | guiFolders.floor.addColor(opts.floor, 'specularColor')
276 |
277 | guiFolders.lights = gui.addFolder('Lights')
278 | guiFolders.lights.add(opts.lights, 'posX', -100, 100)
279 | guiFolders.lights.add(opts.lights, 'posY', 0, 100)
280 | guiFolders.lights.add(opts.lights, 'posZ', -100, 100)
281 | guiFolders.lights.addColor(opts.lights, 'ambientColor')
282 |
283 | guiFolders.teapot = gui.addFolder('Teapot')
284 | guiFolders.teapot.add(opts.teapot, 'shininess', 1, 50)
285 | guiFolders.teapot.addColor(opts.teapot, 'specularColor')
286 |
287 | gl.enable(gl.DEPTH_TEST)
288 | gl.enable(gl.CULL_FACE)
289 |
290 | bunnyGeo.attr('aPos', bunny.positions)
291 | bunnyGeo.attr(
292 | 'aNormal',
293 | normals.vertexNormals(
294 | bunny.cells,
295 | bunny.positions
296 | )
297 | )
298 | bunnyGeo.faces(bunny.cells)
299 | bunnyBoundingBox = boundingBox(bunny.positions)
300 |
301 | floorGeo.attr('aPos', floor.positions)
302 |
303 | floorGeo.attr(
304 | 'aNormal',
305 | normals.vertexNormals(
306 | floor.cells,
307 | floor.positions
308 | )
309 | )
310 | floorGeo.attr('aTexCo', floor.texCos, {size: 2})
311 | floorGeo.faces(floor.cells)
312 |
313 | // TODO (abiro) Use anisotropic filtering.
314 | floorTexture.wrap = [gl.REPEAT, gl.REPEAT]
315 | floorTexture.magFilter = gl.LINEAR
316 | floorTexture.minFilter = gl.LINEAR_MIPMAP_LINEAR
317 | floorTexture.generateMipmap()
318 |
319 | teapotGeo.attr('aPos', teapot.positions)
320 | teapotGeo.attr(
321 | 'aNormal',
322 | normals.vertexNormals(
323 | teapot.cells,
324 | teapot.positions
325 | )
326 | )
327 | teapotGeo.faces(teapot.cells)
328 | teapotBoundingBox = boundingBox(teapot.positions)
329 |
330 | mat4.scale(bunnyModelMatrix, bunnyModelMatrix, [2, 2, 2])
331 | mat4.translate(
332 | bunnyModelMatrix,
333 | bunnyModelMatrix,
334 | [0, -Math.abs(bunnyBoundingBox[0][1]), 0]
335 | )
336 |
337 | mat4.translate(
338 | teapotModelMatrix,
339 | teapotModelMatrix,
340 | [0, Math.abs(teapotBoundingBox[0][1]), 0]
341 | )
342 | mat4.rotateY(teapotModelMatrix, teapotModelMatrix, Math.PI / 2)
343 |
344 | drawObjects()
345 | }
346 |
--------------------------------------------------------------------------------
/demo/lib/fpsmeter.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * FPSMeter 0.3.1 - 9th May 2013
3 | * https://github.com/Darsain/fpsmeter
4 | *
5 | * Licensed under the MIT license.
6 | * http://opensource.org/licenses/MIT
7 | */
8 | ;(function (w, undefined) {
9 | 'use strict';
10 |
11 | /**
12 | * Create a new element.
13 | *
14 | * @param {String} name Element type name.
15 | *
16 | * @return {Element}
17 | */
18 | function newEl(name) {
19 | return document.createElement(name);
20 | }
21 |
22 | /**
23 | * Apply theme CSS properties to element.
24 | *
25 | * @param {Element} element DOM element.
26 | * @param {Object} theme Theme object.
27 | *
28 | * @return {Element}
29 | */
30 | function applyTheme(element, theme) {
31 | for (var name in theme) {
32 | try {
33 | element.style[name] = theme[name];
34 | } catch (e) {}
35 | }
36 | return element;
37 | }
38 |
39 | /**
40 | * Return type of the value.
41 | *
42 | * @param {Mixed} value
43 | *
44 | * @return {String}
45 | */
46 | function type(value) {
47 | if (value == null) {
48 | return String(value);
49 | }
50 |
51 | if (typeof value === 'object' || typeof value === 'function') {
52 | return Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase() || 'object';
53 | }
54 |
55 | return typeof value;
56 | }
57 |
58 | /**
59 | * Check whether the value is in an array.
60 | *
61 | * @param {Mixed} value
62 | * @param {Array} array
63 | *
64 | * @return {Integer} Array index or -1 when not found.
65 | */
66 | function inArray(value, array) {
67 | if (type(array) !== 'array') {
68 | return -1;
69 | }
70 | if (array.indexOf) {
71 | return array.indexOf(value);
72 | }
73 | for (var i = 0, l = array.length; i < l; i++) {
74 | if (array[i] === value) {
75 | return i;
76 | }
77 | }
78 | return -1;
79 | }
80 |
81 | /**
82 | * Poor man's deep object extend.
83 | *
84 | * Example:
85 | * extend({}, defaults, options);
86 | *
87 | * @return {Void}
88 | */
89 | function extend() {
90 | var args = arguments;
91 | for (var key in args[1]) {
92 | if (args[1].hasOwnProperty(key)) {
93 | switch (type(args[1][key])) {
94 | case 'object':
95 | args[0][key] = extend({}, args[0][key], args[1][key]);
96 | break;
97 |
98 | case 'array':
99 | args[0][key] = args[1][key].slice(0);
100 | break;
101 |
102 | default:
103 | args[0][key] = args[1][key];
104 | }
105 | }
106 | }
107 | return args.length > 2 ?
108 | extend.apply(null, [args[0]].concat(Array.prototype.slice.call(args, 2))) :
109 | args[0];
110 | }
111 |
112 | /**
113 | * Convert HSL color to HEX string.
114 | *
115 | * @param {Array} hsl Array with [hue, saturation, lightness].
116 | *
117 | * @return {Array} Array with [red, green, blue].
118 | */
119 | function hslToHex(h, s, l) {
120 | var r, g, b;
121 | var v, min, sv, sextant, fract, vsf;
122 |
123 | if (l <= 0.5) {
124 | v = l * (1 + s);
125 | } else {
126 | v = l + s - l * s;
127 | }
128 |
129 | if (v === 0) {
130 | return '#000';
131 | } else {
132 | min = 2 * l - v;
133 | sv = (v - min) / v;
134 | h = 6 * h;
135 | sextant = Math.floor(h);
136 | fract = h - sextant;
137 | vsf = v * sv * fract;
138 | if (sextant === 0 || sextant === 6) {
139 | r = v;
140 | g = min + vsf;
141 | b = min;
142 | } else if (sextant === 1) {
143 | r = v - vsf;
144 | g = v;
145 | b = min;
146 | } else if (sextant === 2) {
147 | r = min;
148 | g = v;
149 | b = min + vsf;
150 | } else if (sextant === 3) {
151 | r = min;
152 | g = v - vsf;
153 | b = v;
154 | } else if (sextant === 4) {
155 | r = min + vsf;
156 | g = min;
157 | b = v;
158 | } else {
159 | r = v;
160 | g = min;
161 | b = v - vsf;
162 | }
163 | return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
164 | }
165 | }
166 |
167 | /**
168 | * Helper function for hslToHex.
169 | */
170 | function componentToHex(c) {
171 | c = Math.round(c * 255).toString(16);
172 | return c.length === 1 ? '0' + c : c;
173 | }
174 |
175 | /**
176 | * Manage element event listeners.
177 | *
178 | * @param {Node} element
179 | * @param {Event} eventName
180 | * @param {Function} handler
181 | * @param {Bool} remove
182 | *
183 | * @return {Void}
184 | */
185 | function listener(element, eventName, handler, remove) {
186 | if (element.addEventListener) {
187 | element[remove ? 'removeEventListener' : 'addEventListener'](eventName, handler, false);
188 | } else if (element.attachEvent) {
189 | element[remove ? 'detachEvent' : 'attachEvent']('on' + eventName, handler);
190 | }
191 | }
192 |
193 | // Preferred timing funtion
194 | var getTime;
195 | (function () {
196 | var perf = w.performance;
197 | if (perf && (perf.now || perf.webkitNow)) {
198 | var perfNow = perf.now ? 'now' : 'webkitNow';
199 | getTime = perf[perfNow].bind(perf);
200 | } else {
201 | getTime = function () {
202 | return +new Date();
203 | };
204 | }
205 | }());
206 |
207 | // Local WindowAnimationTiming interface polyfill
208 | var cAF = w.cancelAnimationFrame || w.cancelRequestAnimationFrame;
209 | var rAF = w.requestAnimationFrame;
210 | (function () {
211 | var vendors = ['moz', 'webkit', 'o'];
212 | var lastTime = 0;
213 |
214 | // For a more accurate WindowAnimationTiming interface implementation, ditch the native
215 | // requestAnimationFrame when cancelAnimationFrame is not present (older versions of Firefox)
216 | for (var i = 0, l = vendors.length; i < l && !cAF; ++i) {
217 | cAF = w[vendors[i]+'CancelAnimationFrame'] || w[vendors[i]+'CancelRequestAnimationFrame'];
218 | rAF = cAF && w[vendors[i]+'RequestAnimationFrame'];
219 | }
220 |
221 | if (!cAF) {
222 | rAF = function (callback) {
223 | var currTime = getTime();
224 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
225 | lastTime = currTime + timeToCall;
226 | return w.setTimeout(function () { callback(currTime + timeToCall); }, timeToCall);
227 | };
228 |
229 | cAF = function (id) {
230 | clearTimeout(id);
231 | };
232 | }
233 | }());
234 |
235 | // Property name for assigning element text content
236 | var textProp = type(document.createElement('div').textContent) === 'string' ? 'textContent' : 'innerText';
237 |
238 | /**
239 | * FPSMeter class.
240 | *
241 | * @param {Element} anchor Element to append the meter to. Default is document.body.
242 | * @param {Object} options Object with options.
243 | */
244 | function FPSMeter(anchor, options) {
245 | // Optional arguments
246 | if (type(anchor) === 'object' && anchor.nodeType === undefined) {
247 | options = anchor;
248 | anchor = document.body;
249 | }
250 | if (!anchor) {
251 | anchor = document.body;
252 | }
253 |
254 | // Private properties
255 | var self = this;
256 | var o = extend({}, FPSMeter.defaults, options || {});
257 |
258 | var el = {};
259 | var cols = [];
260 | var theme, heatmaps;
261 | var heatDepth = 100;
262 | var heating = [];
263 |
264 | var thisFrameTime = 0;
265 | var frameTime = o.threshold;
266 | var frameStart = 0;
267 | var lastLoop = getTime() - frameTime;
268 | var time;
269 |
270 | var fpsHistory = [];
271 | var durationHistory = [];
272 |
273 | var frameID, renderID;
274 | var showFps = o.show === 'fps';
275 | var graphHeight, count, i, j;
276 |
277 | // Exposed properties
278 | self.options = o;
279 | self.fps = 0;
280 | self.duration = 0;
281 | self.isPaused = 0;
282 |
283 | /**
284 | * Tick start for measuring the actual rendering duration.
285 | *
286 | * @return {Void}
287 | */
288 | self.tickStart = function () {
289 | frameStart = getTime();
290 | };
291 |
292 | /**
293 | * FPS tick.
294 | *
295 | * @return {Void}
296 | */
297 | self.tick = function () {
298 | time = getTime();
299 | thisFrameTime = time - lastLoop;
300 | frameTime += (thisFrameTime - frameTime) / o.smoothing;
301 | self.fps = 1000 / frameTime;
302 | self.duration = frameStart < lastLoop ? frameTime : time - frameStart;
303 | lastLoop = time;
304 | };
305 |
306 | /**
307 | * Pause display rendering.
308 | *
309 | * @return {Object} FPSMeter instance.
310 | */
311 | self.pause = function () {
312 | if (frameID) {
313 | self.isPaused = 1;
314 | clearTimeout(frameID);
315 | cAF(frameID);
316 | cAF(renderID);
317 | frameID = renderID = 0;
318 | }
319 | return self;
320 | };
321 |
322 | /**
323 | * Resume display rendering.
324 | *
325 | * @return {Object} FPSMeter instance.
326 | */
327 | self.resume = function () {
328 | if (!frameID) {
329 | self.isPaused = 0;
330 | requestRender();
331 | }
332 | return self;
333 | };
334 |
335 | /**
336 | * Update options.
337 | *
338 | * @param {String} name Option name.
339 | * @param {Mixed} value New value.
340 | *
341 | * @return {Object} FPSMeter instance.
342 | */
343 | self.set = function (name, value) {
344 | o[name] = value;
345 | showFps = o.show === 'fps';
346 |
347 | // Rebuild or reposition elements when specific option has been updated
348 | if (inArray(name, rebuilders) !== -1) {
349 | createMeter();
350 | }
351 | if (inArray(name, repositioners) !== -1) {
352 | positionMeter();
353 | }
354 | return self;
355 | };
356 |
357 | /**
358 | * Change meter into rendering duration mode.
359 | *
360 | * @return {Object} FPSMeter instance.
361 | */
362 | self.showDuration = function () {
363 | self.set('show', 'ms');
364 | return self;
365 | };
366 |
367 | /**
368 | * Change meter into FPS mode.
369 | *
370 | * @return {Object} FPSMeter instance.
371 | */
372 | self.showFps = function () {
373 | self.set('show', 'fps');
374 | return self;
375 | };
376 |
377 | /**
378 | * Toggles between show: 'fps' and show: 'duration'.
379 | *
380 | * @return {Object} FPSMeter instance.
381 | */
382 | self.toggle = function () {
383 | self.set('show', showFps ? 'ms' : 'fps');
384 | return self;
385 | };
386 |
387 | /**
388 | * Hide the FPSMeter. Also pauses the rendering.
389 | *
390 | * @return {Object} FPSMeter instance.
391 | */
392 | self.hide = function () {
393 | self.pause();
394 | el.container.style.display = 'none';
395 | return self;
396 | };
397 |
398 | /**
399 | * Show the FPSMeter. Also resumes the rendering.
400 | *
401 | * @return {Object} FPSMeter instance.
402 | */
403 | self.show = function () {
404 | self.resume();
405 | el.container.style.display = 'block';
406 | return self;
407 | };
408 |
409 | /**
410 | * Check the current FPS and save it in history.
411 | *
412 | * @return {Void}
413 | */
414 | function historyTick() {
415 | for (i = o.history; i--;) {
416 | fpsHistory[i] = i === 0 ? self.fps : fpsHistory[i-1];
417 | durationHistory[i] = i === 0 ? self.duration : durationHistory[i-1];
418 | }
419 | }
420 |
421 | /**
422 | * Returns heat hex color based on values passed.
423 | *
424 | * @param {Integer} heatmap
425 | * @param {Integer} value
426 | * @param {Integer} min
427 | * @param {Integer} max
428 | *
429 | * @return {Integer}
430 | */
431 | function getHeat(heatmap, value, min, max) {
432 | return heatmaps[0|heatmap][Math.round(Math.min((value - min) / (max - min) * heatDepth, heatDepth))];
433 | }
434 |
435 | /**
436 | * Update counter number and legend.
437 | *
438 | * @return {Void}
439 | */
440 | function updateCounter() {
441 | // Update legend only when changed
442 | if (el.legend.fps !== showFps) {
443 | el.legend.fps = showFps;
444 | el.legend[textProp] = showFps ? 'FPS' : 'ms';
445 | }
446 | // Update counter with a nicely formated & readable number
447 | count = showFps ? self.fps : self.duration;
448 | el.count[textProp] = count > 999 ? '999+' : count.toFixed(count > 99 ? 0 : o.decimals);
449 | }
450 |
451 | /**
452 | * Render current FPS state.
453 | *
454 | * @return {Void}
455 | */
456 | function render() {
457 | time = getTime();
458 | // If renderer stopped reporting, do a simulated drop to 0 fps
459 | if (lastLoop < time - o.threshold) {
460 | self.fps -= self.fps / Math.max(1, o.smoothing * 60 / o.interval);
461 | self.duration = 1000 / self.fps;
462 | }
463 |
464 | historyTick();
465 | updateCounter();
466 |
467 | // Apply heat to elements
468 | if (o.heat) {
469 | if (heating.length) {
470 | for (i = heating.length; i--;) {
471 | heating[i].el.style[theme[heating[i].name].heatOn] = showFps ?
472 | getHeat(theme[heating[i].name].heatmap, self.fps, 0, o.maxFps) :
473 | getHeat(theme[heating[i].name].heatmap, self.duration, o.threshold, 0);
474 | }
475 | }
476 |
477 | if (el.graph && theme.column.heatOn) {
478 | for (i = cols.length; i--;) {
479 | cols[i].style[theme.column.heatOn] = showFps ?
480 | getHeat(theme.column.heatmap, fpsHistory[i], 0, o.maxFps) :
481 | getHeat(theme.column.heatmap, durationHistory[i], o.threshold, 0);
482 | }
483 | }
484 | }
485 |
486 | // Update graph columns height
487 | if (el.graph) {
488 | for (j = 0; j < o.history; j++) {
489 | cols[j].style.height = (showFps ?
490 | (fpsHistory[j] ? Math.round(graphHeight / o.maxFps * Math.min(fpsHistory[j], o.maxFps)) : 0) :
491 | (durationHistory[j] ? Math.round(graphHeight / o.threshold * Math.min(durationHistory[j], o.threshold)) : 0)
492 | ) + 'px';
493 | }
494 | }
495 | }
496 |
497 | /**
498 | * Request rendering loop.
499 | *
500 | * @return {Int} Animation frame index.
501 | */
502 | function requestRender() {
503 | if (o.interval < 20) {
504 | frameID = rAF(requestRender);
505 | render();
506 | } else {
507 | frameID = setTimeout(requestRender, o.interval);
508 | renderID = rAF(render);
509 | }
510 | }
511 |
512 | /**
513 | * Meter events handler.
514 | *
515 | * @return {Void}
516 | */
517 | function eventHandler(event) {
518 | event = event || window.event;
519 | if (event.preventDefault) {
520 | event.preventDefault();
521 | event.stopPropagation();
522 | } else {
523 | event.returnValue = false;
524 | event.cancelBubble = true;
525 | }
526 | self.toggle();
527 | }
528 |
529 | /**
530 | * Destroys the current FPSMeter instance.
531 | *
532 | * @return {Void}
533 | */
534 | self.destroy = function () {
535 | // Stop rendering
536 | self.pause();
537 | // Remove elements
538 | removeMeter();
539 | // Stop listening
540 | self.tick = self.tickStart = function () {};
541 | };
542 |
543 | /**
544 | * Remove meter element.
545 | *
546 | * @return {Void}
547 | */
548 | function removeMeter() {
549 | // Unbind listeners
550 | if (o.toggleOn) {
551 | listener(el.container, o.toggleOn, eventHandler, 1);
552 | }
553 | // Detach element
554 | anchor.removeChild(el.container);
555 | }
556 |
557 | /**
558 | * Sets the theme, and generates heatmaps when needed.
559 | */
560 | function setTheme() {
561 | theme = FPSMeter.theme[o.theme];
562 |
563 | // Generate heatmaps
564 | heatmaps = theme.compiledHeatmaps || [];
565 | if (!heatmaps.length && theme.heatmaps.length) {
566 | for (j = 0; j < theme.heatmaps.length; j++) {
567 | heatmaps[j] = [];
568 | for (i = 0; i <= heatDepth; i++) {
569 | heatmaps[j][i] = hslToHex(0.33 / heatDepth * i, theme.heatmaps[j].saturation, theme.heatmaps[j].lightness);
570 | }
571 | }
572 | theme.compiledHeatmaps = heatmaps;
573 | }
574 | }
575 |
576 | /**
577 | * Creates and attaches the meter element.
578 | *
579 | * @return {Void}
580 | */
581 | function createMeter() {
582 | // Remove old meter if present
583 | if (el.container) {
584 | removeMeter();
585 | }
586 |
587 | // Set theme
588 | setTheme();
589 |
590 | // Create elements
591 | el.container = applyTheme(newEl('div'), theme.container);
592 | el.count = el.container.appendChild(applyTheme(newEl('div'), theme.count));
593 | el.legend = el.container.appendChild(applyTheme(newEl('div'), theme.legend));
594 | el.graph = o.graph ? el.container.appendChild(applyTheme(newEl('div'), theme.graph)) : 0;
595 |
596 | // Add elements to heating array
597 | heating.length = 0;
598 | for (var key in el) {
599 | if (el[key] && theme[key].heatOn) {
600 | heating.push({
601 | name: key,
602 | el: el[key]
603 | });
604 | }
605 | }
606 |
607 | // Graph
608 | cols.length = 0;
609 | if (el.graph) {
610 | // Create graph
611 | el.graph.style.width = (o.history * theme.column.width + (o.history - 1) * theme.column.spacing) + 'px';
612 |
613 | // Add columns
614 | for (i = 0; i < o.history; i++) {
615 | cols[i] = el.graph.appendChild(applyTheme(newEl('div'), theme.column));
616 | cols[i].style.position = 'absolute';
617 | cols[i].style.bottom = 0;
618 | cols[i].style.right = (i * theme.column.width + i * theme.column.spacing) + 'px';
619 | cols[i].style.width = theme.column.width + 'px';
620 | cols[i].style.height = '0px';
621 | }
622 | }
623 |
624 | // Set the initial state
625 | positionMeter();
626 | updateCounter();
627 |
628 | // Append container to anchor
629 | anchor.appendChild(el.container);
630 |
631 | // Retrieve graph height after it was appended to DOM
632 | if (el.graph) {
633 | graphHeight = el.graph.clientHeight;
634 | }
635 |
636 | // Add event listeners
637 | if (o.toggleOn) {
638 | if (o.toggleOn === 'click') {
639 | el.container.style.cursor = 'pointer';
640 | }
641 | listener(el.container, o.toggleOn, eventHandler);
642 | }
643 | }
644 |
645 | /**
646 | * Positions the meter based on options.
647 | *
648 | * @return {Void}
649 | */
650 | function positionMeter() {
651 | applyTheme(el.container, o);
652 | }
653 |
654 | /**
655 | * Construct.
656 | */
657 | (function () {
658 | // Create meter element
659 | createMeter();
660 | // Start rendering
661 | requestRender();
662 | }());
663 | }
664 |
665 | // Expose the extend function
666 | FPSMeter.extend = extend;
667 |
668 | // Expose the FPSMeter class
669 | window.FPSMeter = FPSMeter;
670 |
671 | // Default options
672 | FPSMeter.defaults = {
673 | interval: 100, // Update interval in milliseconds.
674 | smoothing: 10, // Spike smoothing strength. 1 means no smoothing.
675 | show: 'fps', // Whether to show 'fps', or 'ms' = frame duration in milliseconds.
676 | toggleOn: 'click', // Toggle between show 'fps' and 'ms' on this event.
677 | decimals: 1, // Number of decimals in FPS number. 1 = 59.9, 2 = 59.94, ...
678 | maxFps: 60, // Max expected FPS value.
679 | threshold: 100, // Minimal tick reporting interval in milliseconds.
680 |
681 | // Meter position
682 | position: 'absolute', // Meter position.
683 | zIndex: 10, // Meter Z index.
684 | left: '5px', // Meter left offset.
685 | top: '5px', // Meter top offset.
686 | right: 'auto', // Meter right offset.
687 | bottom: 'auto', // Meter bottom offset.
688 | margin: '0 0 0 0', // Meter margin. Helps with centering the counter when left: 50%;
689 |
690 | // Theme
691 | theme: 'dark', // Meter theme. Build in: 'dark', 'light', 'transparent', 'colorful'.
692 | heat: 0, // Allow themes to use coloring by FPS heat. 0 FPS = red, maxFps = green.
693 |
694 | // Graph
695 | graph: 0, // Whether to show history graph.
696 | history: 20 // How many history states to show in a graph.
697 | };
698 |
699 | // Option names that trigger FPSMeter rebuild or reposition when modified
700 | var rebuilders = [
701 | 'toggleOn',
702 | 'theme',
703 | 'heat',
704 | 'graph',
705 | 'history'
706 | ];
707 | var repositioners = [
708 | 'position',
709 | 'zIndex',
710 | 'left',
711 | 'top',
712 | 'right',
713 | 'bottom',
714 | 'margin'
715 | ];
716 | }(window));
717 | ;(function (w, FPSMeter, undefined) {
718 | 'use strict';
719 |
720 | // Themes object
721 | FPSMeter.theme = {};
722 |
723 | // Base theme with layout, no colors
724 | var base = FPSMeter.theme.base = {
725 | heatmaps: [],
726 | container: {
727 | // Settings
728 | heatOn: null,
729 | heatmap: null,
730 |
731 | // Styles
732 | padding: '5px',
733 | minWidth: '95px',
734 | height: '30px',
735 | lineHeight: '30px',
736 | textAlign: 'right',
737 | textShadow: 'none'
738 | },
739 | count: {
740 | // Settings
741 | heatOn: null,
742 | heatmap: null,
743 |
744 | // Styles
745 | position: 'absolute',
746 | top: 0,
747 | right: 0,
748 | padding: '5px 10px',
749 | height: '30px',
750 | fontSize: '24px',
751 | fontFamily: 'Consolas, Andale Mono, monospace',
752 | zIndex: 2
753 | },
754 | legend: {
755 | // Settings
756 | heatOn: null,
757 | heatmap: null,
758 |
759 | // Styles
760 | position: 'absolute',
761 | top: 0,
762 | left: 0,
763 | padding: '5px 10px',
764 | height: '30px',
765 | fontSize: '12px',
766 | lineHeight: '32px',
767 | fontFamily: 'sans-serif',
768 | textAlign: 'left',
769 | zIndex: 2
770 | },
771 | graph: {
772 | // Settings
773 | heatOn: null,
774 | heatmap: null,
775 |
776 | // Styles
777 | position: 'relative',
778 | boxSizing: 'padding-box',
779 | MozBoxSizing: 'padding-box',
780 | height: '100%',
781 | zIndex: 1
782 | },
783 | column: {
784 | // Settings
785 | width: 4,
786 | spacing: 1,
787 | heatOn: null,
788 | heatmap: null
789 | }
790 | };
791 |
792 | // Dark theme
793 | FPSMeter.theme.dark = FPSMeter.extend({}, base, {
794 | heatmaps: [{
795 | saturation: 0.8,
796 | lightness: 0.8
797 | }],
798 | container: {
799 | background: '#222',
800 | color: '#fff',
801 | border: '1px solid #1a1a1a',
802 | textShadow: '1px 1px 0 #222'
803 | },
804 | count: {
805 | heatOn: 'color'
806 | },
807 | column: {
808 | background: '#3f3f3f'
809 | }
810 | });
811 |
812 | // Light theme
813 | FPSMeter.theme.light = FPSMeter.extend({}, base, {
814 | heatmaps: [{
815 | saturation: 0.5,
816 | lightness: 0.5
817 | }],
818 | container: {
819 | color: '#666',
820 | background: '#fff',
821 | textShadow: '1px 1px 0 rgba(255,255,255,.5), -1px -1px 0 rgba(255,255,255,.5)',
822 | boxShadow: '0 0 0 1px rgba(0,0,0,.1)'
823 | },
824 | count: {
825 | heatOn: 'color'
826 | },
827 | column: {
828 | background: '#eaeaea'
829 | }
830 | });
831 |
832 | // Colorful theme
833 | FPSMeter.theme.colorful = FPSMeter.extend({}, base, {
834 | heatmaps: [{
835 | saturation: 0.5,
836 | lightness: 0.6
837 | }],
838 | container: {
839 | heatOn: 'backgroundColor',
840 | background: '#888',
841 | color: '#fff',
842 | textShadow: '1px 1px 0 rgba(0,0,0,.2)',
843 | boxShadow: '0 0 0 1px rgba(0,0,0,.1)'
844 | },
845 | column: {
846 | background: '#777',
847 | backgroundColor: 'rgba(0,0,0,.2)'
848 | }
849 | });
850 |
851 | // Transparent theme
852 | FPSMeter.theme.transparent = FPSMeter.extend({}, base, {
853 | heatmaps: [{
854 | saturation: 0.8,
855 | lightness: 0.5
856 | }],
857 | container: {
858 | padding: 0,
859 | color: '#fff',
860 | textShadow: '1px 1px 0 rgba(0,0,0,.5)'
861 | },
862 | count: {
863 | padding: '0 5px',
864 | height: '40px',
865 | lineHeight: '40px'
866 | },
867 | legend: {
868 | padding: '0 5px',
869 | height: '40px',
870 | lineHeight: '42px'
871 | },
872 | graph: {
873 | height: '40px'
874 | },
875 | column: {
876 | width: 5,
877 | background: '#999',
878 | heatOn: 'backgroundColor',
879 | opacity: 0.5
880 | }
881 | });
882 | }(window, FPSMeter));
883 |
884 | module.exports = window.FPSMeter;
--------------------------------------------------------------------------------