├── .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; --------------------------------------------------------------------------------