├── .babelrc ├── .gitignore ├── LICENSE.md ├── README.md ├── build.js ├── package-lock.json ├── package.json ├── public ├── build │ └── .gitkeep └── index.html ├── server.js └── src ├── config └── postProcessing.js ├── core ├── App.js ├── Camera.js ├── Renderer.js └── Scene.js ├── entities └── Cube │ ├── CubeMaterial.js │ └── index.js ├── helpers └── GUI.js ├── main.js ├── postProcessing ├── Composer.js ├── EffectComposer.js ├── PostProcessing.js ├── functions │ ├── fxaa │ │ └── index.glsl │ ├── rgb │ │ └── index.glsl │ └── tilt-shift │ │ └── index.glsl ├── passes │ ├── BloomPass │ │ ├── BloomPass.js │ │ ├── convolution.frag │ │ └── convolution.vert │ ├── BloomUnrealPass │ │ ├── BloomUnrealPass.js │ │ ├── blur.frag │ │ └── composite.frag │ ├── ClearMaskPass │ │ └── ClearMaskPass.js │ ├── MaskPass │ │ └── MaskPass.js │ ├── Pass.js │ ├── PostPass │ │ ├── PostPass.js │ │ └── post.frag │ ├── RenderPass │ │ └── RenderPass.js │ ├── ShaderPass │ │ └── ShaderPass.js │ └── index.js └── shaders │ ├── basic.frag │ ├── basic.vert │ ├── copy.frag │ └── luminosity-high.frag ├── shaders ├── cube.frag └── cube.vert ├── signals └── Window.js ├── stylesheets └── main.styl └── utils └── math ├── clamp.js ├── index.js ├── lerp.js ├── map.js ├── normalize.js ├── random-float.js ├── random-hex-color.js ├── random-int.js └── to-radians.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules/ 4 | dist/ 5 | doc/ 6 | .history/ 7 | *.log 8 | bundle.js 9 | main.css 10 | *.map 11 | *.log 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2016 [Fabien Motte](http://fabienmotte.com/). 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ternion (ˈtɜːnɪən) 2 | 3 | [![stability][stability-image]][stability-url] 4 | [![js-standard-style][standard-image]][standard-url] 5 | 6 | ![Ternion](http://i.imgur.com/vhIj9en.png) 7 | 8 | A simple and opinionated **starter kit** to **prototype** quickly your ideas with Three.js. 9 | 10 | ## Features 11 | 12 | - **Three.js** and **post processing** 13 | - **dat.GUI** 14 | - Code linted with **Javascript Standard Style** 15 | - [**budō**](https://github.com/mattdesl/budo) (with **LiveReload**) for a fast development server 16 | - ES2015 transpiling with **Babel 6** (*stage 0* enabled) 17 | - Development and production (with **UglifyJS** transform) builds 18 | - **Glslify** transform (vert/frag shaders) 19 | - Some useful **basic functions** and **helpers** 20 | - **Stylus** support for stylesheets 21 | 22 | ## Install 23 | 24 | Clone this repository and install dependencies : 25 | 26 | ```sh 27 | git clone https://github.com/FabienMotte/Ternion.git 28 | ``` 29 | 30 | ```sh 31 | npm install 32 | ``` 33 | 34 | ## Usage 35 | 36 | A **simple example** (see the picture above) is included as a demonstration.
37 | Now it's time to be creative and imagine something on your own ! 38 | 39 | ### Development 40 | 41 | It starts a [budō](https://github.com/mattdesl/budo) server with LiveReload and open [http://localhost:9966/](http://localhost:9966/) for you. 42 | 43 | ```sh 44 | npm start 45 | ``` 46 | 47 | ### Production 48 | 49 | It builds with Browserify a bundled file outputted here : `public/build/bundle.js`. 50 | 51 | ```sh 52 | npm run build 53 | ``` 54 | 55 | ## Contribute 56 | 57 | This starter kit is opinionated, but feel free to submit issues or pull requests ! 58 | 59 | ## Contributors 60 | 61 | - [Patrick Heng](https://github.com/patrickheng) 62 | 63 | ## License 64 | 65 | MIT, see [LICENSE.md](LICENSE.md) for details. 66 | 67 | [stability-image]: https://img.shields.io/badge/stability-stable-brightgreen.svg?style=flat-square 68 | [stability-url]: https://nodejs.org/api/documentation.html#documentation_stability_index 69 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 70 | [standard-url]: https://github.com/feross/standard 71 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const browserify = require('browserify') 3 | 4 | const b = browserify('./src/main.js', { 5 | paths: [ 6 | path.join(__dirname, '/src') 7 | ], 8 | insertGlobalVars: { 9 | THREE: (file, dir) => `require('three')` 10 | } 11 | }) 12 | 13 | b.bundle().pipe(process.stdout) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ternion", 3 | "version": "5.0.0", 4 | "description": "Ternion, a simple starter kit to prototype quickly your ideas with Three.js", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "concurrently -k -r \"node server.js\" \"npm run css:dev\"", 9 | "build": "NODE_ENV='production' node build.js | uglifyjs -c > public/build/bundle.js", 10 | "css:dev": "stylus -u autoprefixer-stylus -w ./src/stylesheets/main.styl -o public/build --sourcemap", 11 | "css:prod": "stylus -u autoprefixer-stylus ./src/stylesheets/main.styl -o public/build --compress", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "budo", 16 | "glsl", 17 | "webgl", 18 | "three.js", 19 | "glslify", 20 | "starter", 21 | "boilerplate", 22 | "prototyping" 23 | ], 24 | "author": "Fabien Motte ", 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/FabienMotte/Ternion.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/FabienMotte/Ternion/issues" 32 | }, 33 | "homepage": "https://github.com/FabienMotte/Ternion", 34 | "dependencies": { 35 | "color-shader-functions": "0.0.2", 36 | "glslify-bare": "FabienMotte/glslify-bare", 37 | "lodash.debounce": "^4.0.8", 38 | "quark-raf": "^1.0.4", 39 | "quark-signal": "^1.1.2", 40 | "raf": "^3.4.0", 41 | "three": "^0.91.0", 42 | "three-orbitcontrols": "^2.0.0" 43 | }, 44 | "devDependencies": { 45 | "autoprefixer-stylus": "^0.14.0", 46 | "babel-core": "^6.26.0", 47 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 48 | "babel-preset-env": "^1.6.1", 49 | "babelify": "^8.0.0", 50 | "browserify": "^16.1.1", 51 | "budo": "^11.2.0", 52 | "concurrently": "^3.5.1", 53 | "dat-gui": "^0.5.0", 54 | "glslify": "^6.1.1", 55 | "stylus": "^0.54.5", 56 | "uglify-js": "^3.3.15" 57 | }, 58 | "browserify": { 59 | "transform": [ 60 | "babelify", 61 | "glslify-bare" 62 | ] 63 | }, 64 | "standard": { 65 | "globals": [ 66 | "THREE", 67 | "TweenMax", 68 | "TimelineMax", 69 | "Back", 70 | "Bounce", 71 | "Circ", 72 | "Cubic", 73 | "Ease", 74 | "Elastic", 75 | "Expo", 76 | "Linear", 77 | "Power0", 78 | "Power1", 79 | "Power2", 80 | "Power3", 81 | "Power4", 82 | "Quad", 83 | "RoughEase", 84 | "Sine", 85 | "SplitText" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabienMotte/Ternion/e14a4e63ca777e3b82b52cf1b8243469d12b26ca/public/build/.gitkeep -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ternion 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const budo = require('budo') 3 | 4 | budo('src/main.js', { 5 | serve: 'build/bundle.js', 6 | live: true, 7 | open: true, 8 | dir: 'public', 9 | stream: process.stdout, 10 | watchGlob: '**/*.{html,css,frag,vert,glsl}', 11 | browserify: { 12 | paths: [ 13 | path.join(__dirname, '/src') 14 | ], 15 | insertGlobalVars: { 16 | THREE: (file, dir) => `require('three')` 17 | } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/config/postProcessing.js: -------------------------------------------------------------------------------- 1 | import { BloomUnrealPass, PostPass } from 'postProcessing/passes' 2 | 3 | export default { 4 | active: true, 5 | passes: [ 6 | { 7 | active: true, 8 | name: 'bloomPass', 9 | constructor: new BloomUnrealPass({ 10 | resolution: new THREE.Vector2(256, 256), 11 | strength: 2, 12 | radius: 1, 13 | threshold: 0.04 14 | }) 15 | }, 16 | { 17 | active: true, 18 | name: 'postPass', 19 | constructor: new PostPass() 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/core/App.js: -------------------------------------------------------------------------------- 1 | import Renderer from 'core/Renderer' 2 | import Camera from 'core/Camera' 3 | import Scene from 'core/Scene' 4 | 5 | class App { 6 | static begin () { 7 | const renderer = new Renderer() 8 | const container = document.getElementById('webgl-container') 9 | container.appendChild(renderer.domElement) 10 | 11 | const camera = new Camera(75, window.innerWidth / window.innerHeight, 0.1, 10000) 12 | camera.position.z = 5 13 | 14 | const scene = new Scene(renderer, camera) // eslint-disable-line 15 | } 16 | } 17 | 18 | export default App 19 | -------------------------------------------------------------------------------- /src/core/Camera.js: -------------------------------------------------------------------------------- 1 | import OrbitControls from 'three-orbitcontrols' 2 | import Window from 'signals/Window' 3 | import GUI from 'helpers/GUI' 4 | 5 | class Camera extends THREE.PerspectiveCamera { 6 | constructor (fov, aspect, near, far) { 7 | super(fov, aspect, near, far) 8 | 9 | this.controls = new OrbitControls(this, document.getElementById('webgl-container')) 10 | this.controls.enabled = true 11 | 12 | this.bind() 13 | this.addGUI() 14 | } 15 | 16 | bind () { 17 | this.onWindowResize = this.onWindowResize.bind(this) 18 | Window.onResize.add(this.onWindowResize) 19 | } 20 | 21 | addGUI () { 22 | GUI.add(this.controls, 'enabled').name('OrbitControls') 23 | } 24 | 25 | update () { 26 | this.controls.update() 27 | } 28 | 29 | onWindowResize (width, height) { 30 | this.aspect = width / height 31 | this.updateProjectionMatrix() 32 | } 33 | } 34 | 35 | export default Camera 36 | -------------------------------------------------------------------------------- /src/core/Renderer.js: -------------------------------------------------------------------------------- 1 | import Window from 'signals/Window' 2 | 3 | class Renderer extends THREE.WebGLRenderer { 4 | constructor (options = { antialias: true, alpha: false }) { 5 | super(options) 6 | 7 | this.setSize(window.innerWidth, window.innerHeight) 8 | this.setPixelRatio(window.devicePixelRatio) 9 | this.setClearColor(0x0a0a0a, 1.0) 10 | 11 | this.bind() 12 | } 13 | 14 | bind () { 15 | this.onWindowResize = this.onWindowResize.bind(this) 16 | Window.onResize.add(this.onWindowResize) 17 | } 18 | 19 | onWindowResize (width, height) { 20 | this.setSize(width, height) 21 | } 22 | } 23 | 24 | export default Renderer 25 | -------------------------------------------------------------------------------- /src/core/Scene.js: -------------------------------------------------------------------------------- 1 | import Raf from 'quark-raf' 2 | import Cube from 'entities/Cube' 3 | import PostProcessing from 'postProcessing/PostProcessing' 4 | 5 | class Scene extends THREE.Scene { 6 | constructor (renderer, camera) { 7 | super() 8 | 9 | this.renderer = renderer 10 | this.camera = camera 11 | this.postProcessing = new PostProcessing(this, this.renderer, this.camera) 12 | 13 | this.bind() 14 | this.createScene() 15 | } 16 | 17 | bind () { 18 | this.render = this.render.bind(this) 19 | Raf.add(this.render) 20 | } 21 | 22 | createScene () { 23 | this.cube = new Cube() 24 | this.add(this.cube) 25 | } 26 | 27 | render (delta, time) { 28 | const cube = this.cube 29 | 30 | cube.rotation.x += 0.01 31 | cube.rotation.y += 0.02 32 | 33 | cube.update(time) 34 | 35 | this.postProcessing.update(delta, time) 36 | this.camera.update() 37 | } 38 | } 39 | 40 | export default Scene 41 | -------------------------------------------------------------------------------- /src/entities/Cube/CubeMaterial.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/cube.vert' 2 | import fragmentShader from 'shaders/cube.frag' 3 | 4 | class CubeMaterial extends THREE.ShaderMaterial { 5 | constructor (options) { 6 | super(options) 7 | 8 | this.vertexShader = vertexShader 9 | this.fragmentShader = fragmentShader 10 | 11 | this.uniforms = { 12 | time: { type: 'f', value: 0.0 }, 13 | color: { type: 'c', value: new THREE.Color(0xffffff) } 14 | } 15 | } 16 | 17 | update (time) { 18 | this.uniforms.time.value = time * 0.3 19 | } 20 | } 21 | 22 | export default CubeMaterial 23 | -------------------------------------------------------------------------------- /src/entities/Cube/index.js: -------------------------------------------------------------------------------- 1 | import CubeMaterial from './CubeMaterial' 2 | import GUI from 'helpers/GUI' 3 | 4 | class Cube extends THREE.Object3D { 5 | constructor () { 6 | super() 7 | 8 | this.geometry = new THREE.BoxGeometry(1, 1, 1) 9 | this.material = new CubeMaterial({ wireframe: true }) 10 | this.mesh = new THREE.Mesh(this.geometry, this.material) 11 | this.add(this.mesh) 12 | 13 | this.addGUI() 14 | } 15 | 16 | addGUI () { 17 | const positionFolder = GUI.addFolder('Cube Position') 18 | const scaleFolder = GUI.addFolder('Cube Scale') 19 | 20 | positionFolder.add(this.position, 'x').name('position x').min(-10).max(10).step(0.5) 21 | positionFolder.add(this.position, 'y').name('position y').min(-10).max(10).step(0.5) 22 | positionFolder.add(this.position, 'z').name('position z').min(-10).max(10).step(0.5) 23 | 24 | scaleFolder.add(this.scale, 'x').name('scale x').min(0).max(10).step(0.1) 25 | scaleFolder.add(this.scale, 'y').name('scale y').min(0).max(10).step(0.1) 26 | scaleFolder.add(this.scale, 'z').name('scale z').min(0).max(10).step(0.1) 27 | } 28 | 29 | update (time) { 30 | this.material.update(time / 1000) 31 | } 32 | } 33 | 34 | export default Cube 35 | -------------------------------------------------------------------------------- /src/helpers/GUI.js: -------------------------------------------------------------------------------- 1 | import dat from 'dat-gui' 2 | 3 | const GUI = new dat.GUI({ autoPlace: false, scrollable: true }) 4 | document.body.appendChild(GUI.domElement) 5 | 6 | export default GUI 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './core/App' 2 | 3 | let the = App.begin() // eslint-disable-line 4 | -------------------------------------------------------------------------------- /src/postProcessing/Composer.js: -------------------------------------------------------------------------------- 1 | import { ShaderPass, MaskPass, ClearMaskPass } from './passes' 2 | 3 | class Composer { 4 | constructor (renderer, renderTarget) { 5 | this.renderer = renderer 6 | 7 | if (renderTarget === undefined) { 8 | const parameters = { 9 | minFilter: THREE.LinearFilter, 10 | magFilter: THREE.LinearFilter, 11 | format: THREE.RGBAFormat, 12 | stencilBuffer: false 13 | } 14 | 15 | const size = this.renderer.getSize() 16 | renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, parameters) 17 | } 18 | 19 | this.renderTarget1 = renderTarget 20 | this.renderTarget2 = renderTarget.clone() 21 | 22 | this.writeBuffer = this.renderTarget1 23 | this.readBuffer = this.renderTarget2 24 | 25 | this.passes = [] 26 | 27 | this.copyPass = new ShaderPass() 28 | } 29 | 30 | swapBuffers () { 31 | const tmp = this.readBuffer 32 | this.readBuffer = this.writeBuffer 33 | this.writeBuffer = tmp 34 | } 35 | 36 | addPass (pass) { 37 | this.passes.push(pass) 38 | 39 | const size = this.renderer.getSize() 40 | const pixelRatio = this.renderer.getPixelRatio() 41 | pass.setSize(size.width * pixelRatio, size.height * pixelRatio) 42 | } 43 | 44 | insertPass (pass, index) { 45 | this.passes.splice(index, 0, pass) 46 | } 47 | 48 | render (delta, time) { 49 | let maskActive = false 50 | 51 | const passesLength = this.passes.length 52 | 53 | for (let i = 0; i < passesLength; i++) { 54 | const pass = this.passes[i] 55 | 56 | pass.render(this.renderer, this.writeBuffer, this.readBuffer, delta, time, maskActive) 57 | 58 | if (pass.needsSwap) { 59 | if (maskActive) { 60 | const context = this.renderer.context 61 | 62 | context.stencilFunc(context.NOTEQUAL, 1, 0xffffffff) 63 | this.copyPass.render(this.renderer, this.writeBuffer, this.readBuffer, delta, time) 64 | context.stencilFunc(context.EQUAL, 1, 0xffffffff) 65 | } 66 | 67 | this.swapBuffers() 68 | } 69 | 70 | if (MaskPass !== undefined) { 71 | if (pass instanceof MaskPass) { 72 | maskActive = true 73 | } else if (pass instanceof ClearMaskPass) { 74 | maskActive = false 75 | } 76 | } 77 | } 78 | } 79 | 80 | reset (renderTarget) { 81 | if (renderTarget === undefined) { 82 | const size = this.renderer.getSize() 83 | 84 | renderTarget = this.renderTarget1.clone() 85 | renderTarget.setSize(size.width, size.height) 86 | } 87 | 88 | this.renderTarget1.dispose() 89 | this.renderTarget2.dispose() 90 | this.renderTarget1 = renderTarget 91 | this.renderTarget2 = renderTarget.clone() 92 | 93 | this.writeBuffer = this.renderTarget1 94 | this.readBuffer = this.renderTarget2 95 | } 96 | 97 | setSize (width, height) { 98 | this.renderTarget1.setSize(width, height) 99 | this.renderTarget2.setSize(width, height) 100 | 101 | for (let i = 0; i < this.passes.length; i++) { 102 | this.passes[i].setSize(width, height) 103 | } 104 | } 105 | } 106 | 107 | export default Composer 108 | -------------------------------------------------------------------------------- /src/postProcessing/EffectComposer.js: -------------------------------------------------------------------------------- 1 | import Composer from './Composer' 2 | import Window from 'signals/Window' 3 | 4 | class EffectComposer extends Composer { 5 | constructor (renderer) { 6 | super(renderer) 7 | 8 | this.renderer = renderer 9 | 10 | this.bind() 11 | } 12 | 13 | bind () { 14 | this.onWindowResize = this.onWindowResize.bind(this) 15 | Window.onResize.add(this.onWindowResize) 16 | } 17 | 18 | onWindowResize (width, height) { 19 | this.setSize(width * this.renderer.getPixelRatio(), height * this.renderer.getPixelRatio()) 20 | } 21 | } 22 | 23 | export default EffectComposer 24 | -------------------------------------------------------------------------------- /src/postProcessing/PostProcessing.js: -------------------------------------------------------------------------------- 1 | import EffectComposer from './EffectComposer' 2 | import { RenderPass } from './passes' 3 | import postProcessing from 'config/postProcessing' 4 | import GUI from 'helpers/GUI' 5 | 6 | class PostProcessing { 7 | constructor (scene, renderer, camera) { 8 | this.scene = scene 9 | 10 | this.camera = camera 11 | 12 | this.renderer = renderer 13 | this.configuration = postProcessing 14 | 15 | this.passes = this.configuration.passes.filter(pass => pass.active) 16 | this.active = this.configuration.active 17 | 18 | this.composer = new EffectComposer(this.renderer) 19 | 20 | this.composer.addPass(new RenderPass(this.scene, this.camera)) 21 | 22 | for (let i = 0; i < this.passes.length; i++) { 23 | this.composer.addPass(this.passes[i].constructor) 24 | if (i === this.passes.length - 1) { 25 | this.passes[i].constructor.renderToScreen = true 26 | } 27 | } 28 | 29 | this.addGUI() 30 | } 31 | 32 | addGUI () { 33 | GUI.add(this, 'active').name('PostProcessing') 34 | } 35 | 36 | getPass (name) { 37 | return this.passes.find(pass => pass.name === name).constructor 38 | } 39 | 40 | update (delta, time) { 41 | if (this.active && this.passes.length) { 42 | this.composer.render(delta, time) 43 | } else { 44 | this.renderer.render(this.scene, this.camera) 45 | } 46 | } 47 | } 48 | 49 | export default PostProcessing 50 | -------------------------------------------------------------------------------- /src/postProcessing/functions/fxaa/index.glsl: -------------------------------------------------------------------------------- 1 | #define FXAA_REDUCE_MIN (1.0 / 128.0) 2 | #define FXAA_REDUCE_MUL (1.0 / 8.0) 3 | #define FXAA_SPAN_MAX 8.0 4 | 5 | vec4 fxaa(sampler2D tInput, vec2 uv, vec2 resolution) { 6 | vec2 res = 1. / resolution; 7 | 8 | vec3 rgbNW = texture2D( tInput, ( uv.xy + vec2( -1.0, -1.0 ) * res ) ).xyz; 9 | vec3 rgbNE = texture2D( tInput, ( uv.xy + vec2( 1.0, -1.0 ) * res ) ).xyz; 10 | vec3 rgbSW = texture2D( tInput, ( uv.xy + vec2( -1.0, 1.0 ) * res ) ).xyz; 11 | vec3 rgbSE = texture2D( tInput, ( uv.xy + vec2( 1.0, 1.0 ) * res ) ).xyz; 12 | vec4 rgbaM = texture2D( tInput, uv.xy * res ); 13 | vec3 rgbM = rgbaM.xyz; 14 | vec3 luma = vec3( 0.299, 0.587, 0.114 ); 15 | 16 | float lumaNW = dot( rgbNW, luma ); 17 | float lumaNE = dot( rgbNE, luma ); 18 | float lumaSW = dot( rgbSW, luma ); 19 | float lumaSE = dot( rgbSE, luma ); 20 | float lumaM = dot( rgbM, luma ); 21 | float lumaMin = min( lumaM, min( min( lumaNW, lumaNE ), min( lumaSW, lumaSE ) ) ); 22 | float lumaMax = max( lumaM, max( max( lumaNW, lumaNE) , max( lumaSW, lumaSE ) ) ); 23 | 24 | vec2 dir; 25 | dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); 26 | dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); 27 | 28 | float dirReduce = max( ( lumaNW + lumaNE + lumaSW + lumaSE ) * ( 0.25 * FXAA_REDUCE_MUL ), FXAA_REDUCE_MIN ); 29 | 30 | float rcpDirMin = 1.0 / ( min( abs( dir.x ), abs( dir.y ) ) + dirReduce ); 31 | dir = min( vec2( FXAA_SPAN_MAX, FXAA_SPAN_MAX), 32 | max( vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), 33 | dir * rcpDirMin)) * res; 34 | vec4 rgbA = (1.0 / 2.0) * ( 35 | texture2D(tInput, uv.xy + dir * (1.0 / 3.0 - 0.5)) + 36 | texture2D(tInput, uv.xy + dir * (2.0 / 3.0 - 0.5))); 37 | vec4 rgbB = rgbA * (1.0 / 2.0) + (1.0 / 4.0) * ( 38 | texture2D(tInput, uv.xy + dir * (0.0 / 3.0 - 0.5)) + 39 | texture2D(tInput, uv.xy + dir * (3.0 / 3.0 - 0.5))); 40 | float lumaB = dot(rgbB, vec4(luma, 0.0)); 41 | 42 | if ( ( lumaB < lumaMin ) || ( lumaB > lumaMax ) ) { 43 | return rgbA; 44 | } else { 45 | return rgbB; 46 | } 47 | } 48 | 49 | #pragma glslify: export(fxaa) 50 | -------------------------------------------------------------------------------- /src/postProcessing/functions/rgb/index.glsl: -------------------------------------------------------------------------------- 1 | vec4 rgb(sampler2D tInput, vec2 pos, vec2 resolution, vec2 delta) { 2 | vec2 dir = pos - vec2(0.5); 3 | float d = .7 * length(dir); 4 | normalize(dir); 5 | vec2 value = d * dir * delta; 6 | 7 | vec4 c1 = texture2D(tInput, pos - value / resolution.x); 8 | vec4 c2 = texture2D(tInput, pos); 9 | vec4 c3 = texture2D(tInput, pos + value / resolution.y); 10 | 11 | return vec4(c1.r, c2.g, c3.b, c1.a + c2.a + c3.b); 12 | } 13 | 14 | #pragma glslify: export(rgb) 15 | -------------------------------------------------------------------------------- /src/postProcessing/functions/tilt-shift/index.glsl: -------------------------------------------------------------------------------- 1 | const float steps = 3.0; 2 | 3 | const float minOffs = (float(steps - 1.0)) / -2.0; 4 | const float maxOffs = (float(steps - 1.0)) / +2.0; 5 | 6 | vec4 tiltShift(sampler2D tInput, vec2 uv, float blurAmount, float center, float stepSize) { 7 | float amount; 8 | vec4 blurred; 9 | 10 | amount = pow((uv.y * center) * 2.0 - 1.0, 2.0) * blurAmount; 11 | 12 | blurred = vec4(0.0, 0.0, 0.0, 1.0); 13 | 14 | for (float offsX = minOffs; offsX <= maxOffs; ++offsX) { 15 | for (float offsY = minOffs; offsY <= maxOffs; ++offsY) { 16 | vec2 temp_vUv = uv.xy; 17 | 18 | temp_vUv.x += offsX * amount * stepSize; 19 | temp_vUv.y += offsY * amount * stepSize; 20 | 21 | blurred += texture2D(tInput, temp_vUv); 22 | } 23 | } 24 | 25 | blurred /= float(steps * steps); 26 | 27 | return blurred; 28 | } 29 | 30 | #pragma glslify: export(tiltShift) 31 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomPass/BloomPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | import copyVertexShader from '../../shaders/basic.vert' 3 | import copyFragmentShader from '../../shaders/copy.frag' 4 | import convolVertexShader from './convolution.vert' 5 | import convolFragmentShader from './convolution.frag' 6 | 7 | const blur = { 8 | x: new THREE.Vector2(0.001953125, 0.0), 9 | y: new THREE.Vector2(0.0, 0.001953125) 10 | } 11 | 12 | class BloomPass extends Pass { 13 | constructor ({ strength = 1, kernelSize = 16, sigma = 5.0, resolution = 256 }) { 14 | super() 15 | 16 | const options = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat } 17 | 18 | // Render targets 19 | this.renderTargetX = new THREE.WebGLRenderTarget(resolution, resolution, options) 20 | this.renderTargetY = new THREE.WebGLRenderTarget(resolution, resolution, options) 21 | 22 | // Copy material 23 | this.materialCopy = new THREE.ShaderMaterial({ 24 | uniforms: { 25 | tInput: { value: null }, 26 | uOpacity: { value: strength } 27 | }, 28 | vertexShader: copyVertexShader, 29 | fragmentShader: copyFragmentShader, 30 | blending: THREE.AdditiveBlending, 31 | transparent: true 32 | }) 33 | 34 | // Convolution material 35 | this.materialConvolution = new THREE.ShaderMaterial({ 36 | defines: { 37 | KERNEL_SIZE_FLOAT: kernelSize.toFixed(1), 38 | KERNEL_SIZE_INT: kernelSize.toFixed(0) 39 | }, 40 | uniforms: { 41 | tInput: { value: null }, 42 | uImageIncrement: { value: new THREE.Vector2() }, 43 | cKernel: { value: [] } 44 | }, 45 | vertexShader: convolVertexShader, 46 | fragmentShader: convolFragmentShader 47 | }) 48 | 49 | this.materialConvolution.uniforms['uImageIncrement'].value = blur.x 50 | this.materialConvolution.uniforms['cKernel'].value = this.buildKernel(sigma, kernelSize) 51 | 52 | this.needsSwap = false 53 | 54 | this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) 55 | this.scene = new THREE.Scene() 56 | 57 | this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null) 58 | this.scene.add(this.quad) 59 | } 60 | 61 | buildKernel (sigma, kernelSize) { 62 | const values = new Array(kernelSize) 63 | 64 | const halfWidth = (kernelSize - 1) * 0.5 65 | 66 | let sum = 0.0 67 | 68 | for (let i = 0; i < kernelSize; ++i) { 69 | values[i] = this.gauss(i - halfWidth, sigma) 70 | sum += values[i] 71 | } 72 | 73 | // Normalize the kernel 74 | for (let i = 0; i < kernelSize; ++i) { 75 | values[i] /= sum 76 | } 77 | 78 | return values 79 | } 80 | 81 | gauss (x, sigma) { 82 | return Math.exp(-(x * x) / (2.0 * sigma * sigma)) 83 | } 84 | 85 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 86 | const oldAutoClear = renderer.autoClear 87 | renderer.autoClear = false 88 | 89 | if (maskActive) { 90 | renderer.context.disable(renderer.context.STENCIL_TEST) 91 | } 92 | 93 | // Render quad with blured scene into texture (convolution pass 1) 94 | this.quad.material = this.materialConvolution 95 | 96 | this.materialConvolution.uniforms['tInput'].value = readBuffer.texture 97 | this.materialConvolution.uniforms['uImageIncrement'].value = blur.x 98 | 99 | renderer.render(this.scene, this.camera, this.renderTargetX, true) 100 | 101 | // Render quad with blured scene into texture (convolution pass 2) 102 | this.materialConvolution.uniforms['tInput'].value = this.renderTargetX.texture 103 | this.materialConvolution.uniforms['uImageIncrement'].value = blur.y 104 | 105 | renderer.render(this.scene, this.camera, this.renderTargetY, true) 106 | 107 | // Render original scene with superimposed blur to texture 108 | this.quad.material = this.materialCopy 109 | 110 | this.materialCopy.uniforms['tInput'].value = this.renderTargetY.texture 111 | 112 | if (maskActive) { 113 | renderer.context.enable(renderer.context.STENCIL_TEST) 114 | } 115 | 116 | renderer.render(this.scene, this.camera, readBuffer, this.clear) 117 | renderer.autoClear = oldAutoClear 118 | } 119 | } 120 | 121 | export default BloomPass 122 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomPass/convolution.frag: -------------------------------------------------------------------------------- 1 | uniform float cKernel[KERNEL_SIZE_INT]; 2 | 3 | uniform sampler2D tInput; 4 | uniform vec2 uImageIncrement; 5 | 6 | varying vec2 vUv; 7 | 8 | void main() { 9 | vec2 imageCoord = vUv; 10 | vec4 sum = vec4(0.0, 0.0, 0.0, 0.0); 11 | 12 | for (int i = 0; i < KERNEL_SIZE_INT; i ++) { 13 | sum += texture2D(tInput, imageCoord) * cKernel[ i ]; 14 | imageCoord += uImageIncrement; 15 | } 16 | 17 | gl_FragColor = sum; 18 | } 19 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomPass/convolution.vert: -------------------------------------------------------------------------------- 1 | uniform vec2 uImageIncrement; 2 | 3 | varying vec2 vUv; 4 | 5 | void main() { 6 | vUv = uv - ((KERNEL_SIZE_FLOAT - 1.0) / 2.0) * uImageIncrement; 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomUnrealPass/BloomUnrealPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | import basicVertexShader from '../../shaders/basic.vert' 3 | import copyFragmentShader from '../../shaders/copy.frag' 4 | import highFragmentShader from '../../shaders/luminosity-high.frag' 5 | import blurFragmentShader from './blur.frag' 6 | import compositeFragmentShader from './composite.frag' 7 | 8 | const blurDirection = { 9 | x: new THREE.Vector2(1.0, 0.0), 10 | y: new THREE.Vector2(0.0, 1.0) 11 | } 12 | 13 | class UnrealBloomPass extends Pass { 14 | constructor ({ resolution = new THREE.Vector2(256, 256), strength = 1, radius = 1, threshold = 0.8 }) { 15 | super() 16 | 17 | this.resolution = resolution 18 | this.strength = strength 19 | this.radius = radius 20 | this.threshold = threshold 21 | 22 | // Render targets 23 | const options = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat } 24 | this.renderTargetsHorizontal = [] 25 | this.renderTargetsVertical = [] 26 | this.nMips = 5 27 | let resx = Math.round(this.resolution.x / 2) 28 | let resy = Math.round(this.resolution.y / 2) 29 | 30 | this.renderTargetBright = new THREE.WebGLRenderTarget(resx, resy, options) 31 | this.renderTargetBright.texture.generateMipmaps = false 32 | 33 | for (let i = 0; i < this.nMips; i++) { 34 | let renderTarget = new THREE.WebGLRenderTarget(resx, resy, options) 35 | renderTarget.texture.generateMipmaps = false 36 | 37 | this.renderTargetsHorizontal.push(renderTarget) 38 | 39 | renderTarget = new THREE.WebGLRenderTarget(resx, resy, options) 40 | 41 | renderTarget.texture.generateMipmaps = false 42 | this.renderTargetsVertical.push(renderTarget) 43 | 44 | resx = Math.round(resx / 2) 45 | resy = Math.round(resy / 2) 46 | } 47 | 48 | // Luminosity high pass material 49 | this.materialHighPassFilter = new THREE.ShaderMaterial({ 50 | uniforms: { 51 | tInput: { value: null }, 52 | luminosityThreshold: { value: 1.0 }, 53 | smoothWidth: { value: 1.0 }, 54 | defaultColor: { value: new THREE.Color(0x000000) }, 55 | defaultOpacity: { value: 0.0 } 56 | }, 57 | vertexShader: basicVertexShader, 58 | fragmentShader: highFragmentShader 59 | }) 60 | 61 | this.materialHighPassFilter.uniforms['luminosityThreshold'].value = threshold 62 | this.materialHighPassFilter.uniforms['smoothWidth'].value = 0.01 63 | 64 | // Gaussian Blur Materials 65 | this.separableBlurMaterials = [] 66 | const kernelSizeArray = [3, 5, 7, 9, 11] 67 | 68 | for (let i = 0; i < this.nMips; i++) { 69 | const kernelRadius = kernelSizeArray[i] 70 | const seperableBlurMaterial = new THREE.ShaderMaterial({ 71 | defines: { 72 | KERNEL_RADIUS: kernelRadius, 73 | SIGMA: kernelRadius 74 | }, 75 | uniforms: { 76 | colorTexture: { value: null }, 77 | texSize: { value: new THREE.Vector2(0.5, 0.5) }, 78 | direction: { value: new THREE.Vector2(0.5, 0.5) } 79 | }, 80 | vertexShader: basicVertexShader, 81 | fragmentShader: blurFragmentShader 82 | }) 83 | this.separableBlurMaterials.push(seperableBlurMaterial) 84 | this.separableBlurMaterials[i].uniforms['texSize'].value = new THREE.Vector2(resx, resy) 85 | 86 | resx = Math.round(resx / 2) 87 | resy = Math.round(resy / 2) 88 | } 89 | 90 | // Composite material 91 | this.compositeMaterial = new THREE.ShaderMaterial({ 92 | defines: { 93 | NUM_MIPS: this.nMips 94 | }, 95 | uniforms: { 96 | blurTexture1: { value: null }, 97 | blurTexture2: { value: null }, 98 | blurTexture3: { value: null }, 99 | blurTexture4: { value: null }, 100 | blurTexture5: { value: null }, 101 | dirtTexture: { value: null }, 102 | bloomStrength: { value: 1.0 }, 103 | bloomFactors: { value: null }, 104 | bloomTintColors: { value: null }, 105 | bloomRadius: { value: 0.0 } 106 | }, 107 | vertexShader: basicVertexShader, 108 | fragmentShader: compositeFragmentShader 109 | }) 110 | this.compositeMaterial.uniforms['blurTexture1'].value = this.renderTargetsVertical[0].texture 111 | this.compositeMaterial.uniforms['blurTexture2'].value = this.renderTargetsVertical[1].texture 112 | this.compositeMaterial.uniforms['blurTexture3'].value = this.renderTargetsVertical[2].texture 113 | this.compositeMaterial.uniforms['blurTexture4'].value = this.renderTargetsVertical[3].texture 114 | this.compositeMaterial.uniforms['blurTexture5'].value = this.renderTargetsVertical[4].texture 115 | this.compositeMaterial.uniforms['bloomStrength'].value = strength 116 | this.compositeMaterial.uniforms['bloomRadius'].value = 0.1 117 | this.compositeMaterial.needsUpdate = true 118 | 119 | const bloomFactors = [1.0, 0.8, 0.6, 0.4, 0.2] 120 | this.compositeMaterial.uniforms['bloomFactors'].value = bloomFactors 121 | this.bloomTintColors = [ 122 | new THREE.Vector3(1, 1, 1), 123 | new THREE.Vector3(1, 1, 1), 124 | new THREE.Vector3(1, 1, 1), 125 | new THREE.Vector3(1, 1, 1), 126 | new THREE.Vector3(1, 1, 1) 127 | ] 128 | this.compositeMaterial.uniforms['bloomTintColors'].value = this.bloomTintColors 129 | 130 | // Copy material 131 | this.materialCopy = new THREE.ShaderMaterial({ 132 | uniforms: { 133 | tInput: { value: null }, 134 | uOpacity: { value: 1.0 } 135 | }, 136 | vertexShader: basicVertexShader, 137 | fragmentShader: copyFragmentShader, 138 | blending: THREE.AdditiveBlending, 139 | depthTest: false, 140 | depthWrite: false, 141 | transparent: true 142 | }) 143 | 144 | this.enabled = true 145 | this.needsSwap = false 146 | 147 | this.oldClearColor = new THREE.Color() 148 | this.oldClearAlpha = 1 149 | 150 | this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) 151 | this.scene = new THREE.Scene() 152 | 153 | this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null) 154 | this.scene.add(this.quad) 155 | } 156 | 157 | dispose () { 158 | for (let i = 0; i < this.renderTargetsHorizontal.length(); i++) { 159 | this.renderTargetsHorizontal[i].dispose() 160 | } 161 | 162 | for (let i = 0; i < this.renderTargetsVertical.length(); i++) { 163 | this.renderTargetsVertical[i].dispose() 164 | } 165 | 166 | this.renderTargetBright.dispose() 167 | } 168 | 169 | setSize (width, height) { 170 | let resx = Math.round(width / 2) 171 | let resy = Math.round(height / 2) 172 | 173 | this.renderTargetBright.setSize(resx, resy) 174 | 175 | for (let i = 0; i < this.nMips; i++) { 176 | this.renderTargetsHorizontal[i].setSize(resx, resy) 177 | this.renderTargetsVertical[i].setSize(resx, resy) 178 | 179 | this.separableBlurMaterials[i].uniforms['texSize'].value = new THREE.Vector2(resx, resy) 180 | 181 | resx = Math.round(resx / 2) 182 | resy = Math.round(resy / 2) 183 | } 184 | } 185 | 186 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 187 | this.oldClearColor.copy(renderer.getClearColor()) 188 | this.oldClearAlpha = renderer.getClearAlpha() 189 | const oldAutoClear = renderer.autoClear 190 | renderer.autoClear = false 191 | 192 | renderer.setClearColor(new THREE.Color(0, 0, 0), 0) 193 | 194 | if (maskActive) { 195 | renderer.context.disable(renderer.context.STENCIL_TEST) 196 | } 197 | 198 | // 1. Extract Bright Areas 199 | this.materialHighPassFilter.uniforms['tInput'].value = readBuffer.texture 200 | this.materialHighPassFilter.uniforms['luminosityThreshold'].value = this.threshold 201 | this.quad.material = this.materialHighPassFilter 202 | renderer.render(this.scene, this.camera, this.renderTargetBright, true) 203 | 204 | // 2. Blur All the mips progressively 205 | let inputRenderTarget = this.renderTargetBright 206 | 207 | for (let i = 0; i < this.nMips; i++) { 208 | this.quad.material = this.separableBlurMaterials[i] 209 | 210 | this.separableBlurMaterials[i].uniforms['colorTexture'].value = inputRenderTarget.texture 211 | this.separableBlurMaterials[i].uniforms['direction'].value = blurDirection.x 212 | 213 | renderer.render(this.scene, this.camera, this.renderTargetsHorizontal[i], true) 214 | 215 | this.separableBlurMaterials[i].uniforms['colorTexture'].value = this.renderTargetsHorizontal[i].texture 216 | this.separableBlurMaterials[i].uniforms['direction'].value = blurDirection.y 217 | 218 | renderer.render(this.scene, this.camera, this.renderTargetsVertical[i], true) 219 | 220 | inputRenderTarget = this.renderTargetsVertical[i] 221 | } 222 | 223 | // Composite All the mips 224 | this.quad.material = this.compositeMaterial 225 | this.compositeMaterial.uniforms['bloomStrength'].value = this.strength 226 | this.compositeMaterial.uniforms['bloomRadius'].value = this.radius 227 | this.compositeMaterial.uniforms['bloomTintColors'].value = this.bloomTintColors 228 | renderer.render(this.scene, this.camera, this.renderTargetsHorizontal[0], true) 229 | 230 | // Blend it additively over the input texture 231 | this.quad.material = this.materialCopy 232 | this.materialCopy.uniforms['tInput'].value = this.renderTargetsHorizontal[0].texture 233 | 234 | if (maskActive) { 235 | renderer.context.enable(renderer.context.STENCIL_TEST) 236 | } 237 | 238 | renderer.render(this.scene, this.camera, readBuffer, this.clear) 239 | 240 | renderer.setClearColor(this.oldClearColor, this.oldClearAlpha) 241 | renderer.autoClear = oldAutoClear 242 | } 243 | } 244 | 245 | export default UnrealBloomPass 246 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomUnrealPass/blur.frag: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | uniform sampler2D colorTexture; 4 | uniform vec2 texSize; 5 | uniform vec2 direction; 6 | 7 | varying vec2 vUv; 8 | 9 | float gaussianPdf(in float x, in float sigma) { 10 | return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma; 11 | } 12 | 13 | void main() { 14 | vec2 invSize = 1.0 / texSize; 15 | float fSigma = float(SIGMA); 16 | float weightSum = gaussianPdf(0.0, fSigma); 17 | vec3 diffuseSum = texture2D(colorTexture, vUv).rgb * weightSum; 18 | 19 | for (int i = 1; i < KERNEL_RADIUS; i ++) { 20 | float x = float(i); 21 | float w = gaussianPdf(x, fSigma); 22 | vec2 uvOffset = direction * invSize * x; 23 | vec3 sample1 = texture2D(colorTexture, vUv + uvOffset).rgb; 24 | vec3 sample2 = texture2D(colorTexture, vUv - uvOffset).rgb; 25 | diffuseSum += (sample1 + sample2) * w; 26 | weightSum += 2.0 * w; 27 | } 28 | 29 | gl_FragColor = vec4(diffuseSum / weightSum, 1.0); 30 | } 31 | -------------------------------------------------------------------------------- /src/postProcessing/passes/BloomUnrealPass/composite.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D blurTexture1; 2 | uniform sampler2D blurTexture2; 3 | uniform sampler2D blurTexture3; 4 | uniform sampler2D blurTexture4; 5 | uniform sampler2D blurTexture5; 6 | uniform sampler2D dirtTexture; 7 | 8 | uniform float bloomStrength; 9 | uniform float bloomRadius; 10 | 11 | uniform float bloomFactors[NUM_MIPS]; 12 | uniform vec3 bloomTintColors[NUM_MIPS]; 13 | 14 | varying vec2 vUv; 15 | 16 | float lerpBloomFactor(const in float factor) { 17 | float mirrorFactor = 1.2 - factor; 18 | return mix(factor, mirrorFactor, bloomRadius); 19 | } 20 | 21 | void main() { 22 | gl_FragColor = bloomStrength * (lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + 23 | lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + 24 | lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + 25 | lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + 26 | lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv)); 27 | } 28 | -------------------------------------------------------------------------------- /src/postProcessing/passes/ClearMaskPass/ClearMaskPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | 3 | class ClearMaskPass extends Pass { 4 | constructor () { 5 | super() 6 | 7 | this.needsSwap = false 8 | } 9 | 10 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 11 | renderer.state.buffers.stencil.setTest(false) 12 | } 13 | } 14 | 15 | export default ClearMaskPass 16 | -------------------------------------------------------------------------------- /src/postProcessing/passes/MaskPass/MaskPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | 3 | class MaskPass extends Pass { 4 | constructor (scene, camera) { 5 | super() 6 | 7 | this.scene = scene 8 | this.camera = camera 9 | 10 | this.clear = true 11 | this.needsSwap = false 12 | 13 | this.inverse = false 14 | } 15 | 16 | render (renderer, writeBuffer, readBuffer, delta, maskActive) { 17 | const context = renderer.context 18 | const state = renderer.state 19 | 20 | // don't update color or depth 21 | state.buffers.color.setMask(false) 22 | state.buffers.depth.setMask(false) 23 | 24 | // lock buffers 25 | state.buffers.color.setLocked(true) 26 | state.buffers.depth.setLocked(true) 27 | 28 | // set up stencil 29 | var writeValue, clearValue 30 | 31 | if (this.inverse) { 32 | writeValue = 0 33 | clearValue = 1 34 | } else { 35 | writeValue = 1 36 | clearValue = 0 37 | } 38 | 39 | state.buffers.stencil.setTest(true) 40 | state.buffers.stencil.setOp(context.REPLACE, context.REPLACE, context.REPLACE) 41 | state.buffers.stencil.setFunc(context.ALWAYS, writeValue, 0xffffffff) 42 | state.buffers.stencil.setClear(clearValue) 43 | 44 | // draw into the stencil buffer 45 | renderer.render(this.scene, this.camera, readBuffer, this.clear) 46 | renderer.render(this.scene, this.camera, writeBuffer, this.clear) 47 | 48 | // unlock color and depth buffer for subsequent rendering 49 | state.buffers.color.setLocked(false) 50 | state.buffers.depth.setLocked(false) 51 | 52 | // only render where stencil is set to 1 53 | state.buffers.stencil.setFunc(context.EQUAL, 1, 0xffffffff) // draw if == 1 54 | state.buffers.stencil.setOp(context.KEEP, context.KEEP, context.KEEP) 55 | } 56 | } 57 | 58 | export default MaskPass 59 | -------------------------------------------------------------------------------- /src/postProcessing/passes/Pass.js: -------------------------------------------------------------------------------- 1 | class Pass { 2 | constructor () { 3 | // If set to true, the pass indicates to swap read and write buffer after rendering 4 | this.needsSwap = true 5 | 6 | // If set to true, the pass clears its buffer before rendering 7 | this.clear = false 8 | 9 | // If set to true, the result of the pass is rendered to screen 10 | this.renderToScreen = false 11 | } 12 | 13 | setSize (width, height) {} 14 | 15 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 16 | console.error('THREE.Pass: .render() must be implemented in derived pass.') 17 | } 18 | } 19 | 20 | export default Pass 21 | -------------------------------------------------------------------------------- /src/postProcessing/passes/PostPass/PostPass.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from '../ShaderPass/ShaderPass' 2 | import fragmentShader from './post.frag' 3 | 4 | class PostPass extends ShaderPass { 5 | constructor () { 6 | super() 7 | 8 | this.material.uniforms = { 9 | ...this.material.uniforms, 10 | uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, 11 | uTime: { value: 0 }, 12 | uVignette: { value: 1.9 }, 13 | uRgbSplit: { value: new THREE.Vector2() } 14 | } 15 | 16 | this.material.fragmentShader = fragmentShader 17 | } 18 | 19 | setSize (width, height) { 20 | this.material.uniforms.uResolution.value = new THREE.Vector2(width, height) 21 | } 22 | 23 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 24 | super.render(renderer, writeBuffer, readBuffer, delta, time, maskActive) 25 | 26 | this.material.uniforms.uTime.value = time 27 | renderer.autoClear = false 28 | } 29 | } 30 | 31 | export default PostPass 32 | -------------------------------------------------------------------------------- /src/postProcessing/passes/PostPass/post.frag: -------------------------------------------------------------------------------- 1 | #pragma glslify: fxaa = require('../../functions/fxaa'); 2 | #pragma glslify: vignette = require('color-shader-functions/vignette'); 3 | 4 | uniform sampler2D tInput; 5 | uniform float uTime; 6 | uniform float uVignette; 7 | uniform vec2 uResolution; 8 | uniform vec2 uRgbSplit; 9 | 10 | varying vec2 vUv; 11 | 12 | void main() { 13 | vec4 color = texture2D(tInput, vUv); 14 | 15 | color = fxaa(tInput, vUv, uResolution); 16 | color = vignette(color, 1.1, uVignette, uResolution); 17 | 18 | gl_FragColor = color; 19 | } 20 | -------------------------------------------------------------------------------- /src/postProcessing/passes/RenderPass/RenderPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | 3 | class RenderPass extends Pass { 4 | constructor (scene, camera, overrideMaterial, clearColor, clearAlpha = 0) { 5 | super() 6 | 7 | this.scene = scene 8 | this.camera = camera 9 | 10 | this.overrideMaterial = overrideMaterial 11 | 12 | this.clearColor = clearColor 13 | this.clearAlpha = clearAlpha 14 | 15 | this.clear = true 16 | this.needsSwap = false 17 | } 18 | 19 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 20 | const oldAutoClear = renderer.autoClear 21 | renderer.autoClear = false 22 | 23 | this.scene.overrideMaterial = this.overrideMaterial 24 | 25 | let oldClearColor, oldClearAlpha 26 | 27 | if (this.clearColor) { 28 | oldClearColor = renderer.getClearColor().getHex() 29 | oldClearAlpha = renderer.getClearAlpha() 30 | 31 | renderer.setClearColor(this.clearColor, this.clearAlpha) 32 | } 33 | 34 | renderer.render(this.scene, this.camera, this.renderToScreen ? null : readBuffer, this.clear) 35 | 36 | if (this.clearColor) { 37 | renderer.setClearColor(oldClearColor, oldClearAlpha) 38 | } 39 | 40 | this.scene.overrideMaterial = null 41 | renderer.autoClear = oldAutoClear 42 | } 43 | } 44 | 45 | export default RenderPass 46 | -------------------------------------------------------------------------------- /src/postProcessing/passes/ShaderPass/ShaderPass.js: -------------------------------------------------------------------------------- 1 | import Pass from '../Pass' 2 | import vertexShader from '../../shaders/basic.vert' 3 | import fragmentShader from '../../shaders/copy.frag' 4 | 5 | class ShaderPass extends Pass { 6 | constructor (shader) { 7 | super() 8 | 9 | this.material = new THREE.ShaderMaterial({ 10 | uniforms: { 11 | tInput: { value: null }, 12 | uOpacity: { value: 1.0 } 13 | }, 14 | vertexShader, 15 | fragmentShader 16 | }) 17 | 18 | this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) 19 | this.scene = new THREE.Scene() 20 | 21 | this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), this.material) 22 | this.scene.add(this.quad) 23 | } 24 | 25 | render (renderer, writeBuffer, readBuffer, delta, time, maskActive) { 26 | this.material.uniforms['tInput'].value = readBuffer.texture 27 | 28 | if (this.renderToScreen) { 29 | renderer.render(this.scene, this.camera) 30 | } else { 31 | renderer.render(this.scene, this.camera, writeBuffer, this.clear) 32 | } 33 | } 34 | } 35 | 36 | export default ShaderPass 37 | -------------------------------------------------------------------------------- /src/postProcessing/passes/index.js: -------------------------------------------------------------------------------- 1 | import RenderPass from './RenderPass/RenderPass' 2 | import PostPass from './PostPass/PostPass' 3 | import BloomPass from './BloomPass/BloomPass' 4 | import BloomUnrealPass from './BloomUnrealPass/BloomUnrealPass' 5 | import MaskPass from './MaskPass/MaskPass' 6 | import ShaderPass from './ShaderPass/ShaderPass' 7 | import ClearMaskPass from './ClearMaskPass/ClearMaskPass' 8 | 9 | export { 10 | RenderPass, 11 | PostPass, 12 | BloomPass, 13 | BloomUnrealPass, 14 | MaskPass, 15 | ShaderPass, 16 | ClearMaskPass 17 | } 18 | -------------------------------------------------------------------------------- /src/postProcessing/shaders/basic.frag: -------------------------------------------------------------------------------- 1 | void main() { 2 | gl_FragColor = vec4(1.0); 3 | } 4 | -------------------------------------------------------------------------------- /src/postProcessing/shaders/basic.vert: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vUv = uv; 5 | 6 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 7 | } 8 | -------------------------------------------------------------------------------- /src/postProcessing/shaders/copy.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D tInput; 2 | uniform float uOpacity; 3 | 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vec4 color = texture2D(tInput, vUv); 8 | gl_FragColor = color * uOpacity; 9 | } 10 | -------------------------------------------------------------------------------- /src/postProcessing/shaders/luminosity-high.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D tInput; 2 | uniform vec3 defaultColor; 3 | uniform float defaultOpacity; 4 | uniform float luminosityThreshold; 5 | uniform float smoothWidth; 6 | 7 | varying vec2 vUv; 8 | 9 | void main() { 10 | 11 | vec4 texel = texture2D(tInput, vUv); 12 | vec3 luma = vec3(0.299, 0.587, 0.114 ); 13 | float v = dot(texel.xyz, luma); 14 | 15 | vec4 outputColor = vec4(defaultColor.rgb, defaultOpacity); 16 | float alpha = smoothstep(luminosityThreshold, luminosityThreshold + smoothWidth, v); 17 | 18 | gl_FragColor = mix(outputColor, texel, alpha); 19 | } 20 | -------------------------------------------------------------------------------- /src/shaders/cube.frag: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform vec3 color; 3 | 4 | void main() { 5 | 6 | vec3 newColor = vec3(color.r * sin(time), color.g * sin(time + 10.0), color.b * sin(time + 20.0)); 7 | gl_FragColor = vec4(newColor, 1.0); 8 | 9 | } -------------------------------------------------------------------------------- /src/shaders/cube.vert: -------------------------------------------------------------------------------- 1 | void main() { 2 | 3 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 4 | 5 | } -------------------------------------------------------------------------------- /src/signals/Window.js: -------------------------------------------------------------------------------- 1 | import Signal from 'quark-signal' 2 | import debounce from 'lodash.debounce' 3 | 4 | class Window { 5 | constructor () { 6 | this.width = window.innerWidth 7 | this.height = window.innerHeight 8 | 9 | this.createSignals() 10 | this.bind() 11 | } 12 | 13 | createSignals () { 14 | this.onResize = new Signal() 15 | } 16 | 17 | bind () { 18 | this.handleResize = this.handleResize.bind(this) 19 | window.addEventListener('resize', debounce(this.handleResize, 100)) 20 | } 21 | 22 | handleResize () { 23 | this.width = window.innerWidth 24 | this.height = window.innerHeight 25 | this.onResize.dispatch(this.width, this.height) 26 | } 27 | } 28 | 29 | export default new Window() 30 | -------------------------------------------------------------------------------- /src/stylesheets/main.styl: -------------------------------------------------------------------------------- 1 | html, 2 | body 3 | width 100% 4 | height 100% 5 | margin 0 6 | padding 0 7 | overflow hidden 8 | 9 | #webgl-container 10 | width 100% 11 | height 100% 12 | 13 | /* dat.gui */ 14 | .dg.main 15 | position absolute 16 | top 0 17 | right 0 18 | -------------------------------------------------------------------------------- /src/utils/math/clamp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp a value between two bounds 3 | * 4 | * @param {number} min Minimum boundary 5 | * @param {number} max Maximum boundary 6 | * @param {number} v Value to clamp 7 | * @return {number} Clamped value 8 | */ 9 | export default function clamp (min, max, v) { 10 | if (v < min) return min 11 | if (v > max) return max 12 | return v 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/math/index.js: -------------------------------------------------------------------------------- 1 | export clamp from './clamp' 2 | export lerp from './lerp' 3 | export map from './map' 4 | export normalize from './normalize' 5 | export randomFloat from './random-float' 6 | export randomHexColor from './random-hex-color' 7 | export randomInt from './random-int' 8 | export toRadians from './to-radians' 9 | -------------------------------------------------------------------------------- /src/utils/math/lerp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Linear interpolation between two values (lerping) 3 | * 4 | * @param {number} x First point 5 | * @param {number} y Second point 6 | * @param {number} r Value to interpolate 7 | * @return {number} Lerped value 8 | */ 9 | export default function lerp (x, y, r) { 10 | return x + ((y - x) * r) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/math/map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Re-maps a number from one range to another 3 | * 4 | * @param {number} value The incoming value to be converted 5 | * @param {number} start1 Lower bound of the value's current range 6 | * @param {number} stop1 Upper bound of the value's current range 7 | * @param {number} start2 Lower bound of the value's target range 8 | * @param {number} stop2 Upper bound of the value's target range 9 | * @return {number} Remapped number 10 | */ 11 | export default function map (value, start1, stop1, start2, stop2) { 12 | return ((value - start1) / (stop1 - start1)) * (stop2 - start2) + start2 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/math/normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalize a value between two bounds 3 | * 4 | * @param {number} min Minimum boundary 5 | * @param {number} max Maximum boundary 6 | * @param {number} x Value to normalize 7 | * @return {number} Normalized value 8 | */ 9 | export default function normalize (min, max, x) { 10 | return (x - min) / (max - min) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/math/random-float.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random float 3 | * 4 | * @param {number} minValue Minimum boundary 5 | * @param {number} maxValue Maximum boundary 6 | * @param {number} precision Precision 7 | * @return {number} Generated float 8 | */ 9 | export default function randomFloat (minValue, maxValue, precision = 2) { 10 | return parseFloat(Math.min(minValue + (Math.random() * (maxValue - minValue)), maxValue).toFixed(precision)) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/math/random-hex-color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random hexadecimal color 3 | * 4 | * @return {string} Hexadecimal color 5 | */ 6 | export default function randomHexColor () { 7 | return '#' + Math.floor(Math.random() * 16777215).toString(16) 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/math/random-int.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random integer 3 | * 4 | * @param {number} min Minimum boundary 5 | * @param {number} max Maximum boundary 6 | * @return {number} Generated integer 7 | */ 8 | export default function randomInt (min, max) { 9 | return Math.floor(Math.random() * (max - min + 1) + min) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/math/to-radians.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a degrees value into radians 3 | * 4 | * @param {number} degrees Degrees 5 | * @return {number} Radians 6 | */ 7 | export default function toRadians (degrees) { 8 | return degrees * Math.PI / 180 9 | } 10 | --------------------------------------------------------------------------------