Water shader using Three.js, Orbit Controls & TweenMax.
A Pen by Arnaud Rocca on CodePen.
| <div class="app"></div> | |
| <div class="infos"> | |
| <p> | |
| Experiment by <a href="http://www.arnaudrocca.fr" target="_blank">Arnaud Rocca</a> | |
| </p> | |
| </div> | |
| <script id="planeVS" type="shader"> | |
| precision mediump float; | |
| uniform float u_amplitude; | |
| uniform float u_frequency; | |
| uniform float u_time; | |
| // https://github.com/ashima/webgl-noise/blob/master/src/classicnoise3D.glsl | |
| // | |
| // GLSL textureless classic 3D noise "cnoise". | |
| // Author: Stefan Gustavson (stefan.gustavson@liu.se) | |
| // Version: 2011-10-11 | |
| // | |
| // Many thanks to Ian McEwan of Ashima Arts for the | |
| // ideas for permutation and gradient selection. | |
| // | |
| // Copyright (c) 2011 Stefan Gustavson. All rights reserved. | |
| // Distributed under the MIT license. See LICENSE file. | |
| // https://github.com/stegu/webgl-noise | |
| // | |
| vec3 mod289(vec3 x) { | |
| return x - floor(x * (1.0 / 289.0)) * 289.0; | |
| } | |
| vec4 mod289(vec4 x) { | |
| return x - floor(x * (1.0 / 289.0)) * 289.0; | |
| } | |
| vec4 permute(vec4 x) { | |
| return mod289(((x*34.0)+1.0)*x); | |
| } | |
| vec4 taylorInvSqrt(vec4 r) { | |
| return 1.79284291400159 - 0.85373472095314 * r; | |
| } | |
| vec3 fade(vec3 t) { | |
| return t*t*t*(t*(t*6.0-15.0)+10.0); | |
| } | |
| // Classic Perlin noise | |
| float cnoise(vec3 P) { | |
| vec3 Pi0 = floor(P); // Integer part for indexing | |
| vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 | |
| Pi0 = mod289(Pi0); | |
| Pi1 = mod289(Pi1); | |
| vec3 Pf0 = fract(P); // Fractional part for interpolation | |
| vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 | |
| vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); | |
| vec4 iy = vec4(Pi0.yy, Pi1.yy); | |
| vec4 iz0 = Pi0.zzzz; | |
| vec4 iz1 = Pi1.zzzz; | |
| vec4 ixy = permute(permute(ix) + iy); | |
| vec4 ixy0 = permute(ixy + iz0); | |
| vec4 ixy1 = permute(ixy + iz1); | |
| vec4 gx0 = ixy0 * (1.0 / 7.0); | |
| vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; | |
| gx0 = fract(gx0); | |
| vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); | |
| vec4 sz0 = step(gz0, vec4(0.0)); | |
| gx0 -= sz0 * (step(0.0, gx0) - 0.5); | |
| gy0 -= sz0 * (step(0.0, gy0) - 0.5); | |
| vec4 gx1 = ixy1 * (1.0 / 7.0); | |
| vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; | |
| gx1 = fract(gx1); | |
| vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); | |
| vec4 sz1 = step(gz1, vec4(0.0)); | |
| gx1 -= sz1 * (step(0.0, gx1) - 0.5); | |
| gy1 -= sz1 * (step(0.0, gy1) - 0.5); | |
| vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); | |
| vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); | |
| vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); | |
| vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); | |
| vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); | |
| vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); | |
| vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); | |
| vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); | |
| vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); | |
| g000 *= norm0.x; | |
| g010 *= norm0.y; | |
| g100 *= norm0.z; | |
| g110 *= norm0.w; | |
| vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); | |
| g001 *= norm1.x; | |
| g011 *= norm1.y; | |
| g101 *= norm1.z; | |
| g111 *= norm1.w; | |
| float n000 = dot(g000, Pf0); | |
| float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); | |
| float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); | |
| float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); | |
| float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); | |
| float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); | |
| float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); | |
| float n111 = dot(g111, Pf1); | |
| vec3 fade_xyz = fade(Pf0); | |
| vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); | |
| vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); | |
| float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); | |
| return 2.2 * n_xyz; | |
| } | |
| void main() { | |
| // Get a 3d noise using the position | |
| float displacement = u_amplitude * cnoise(u_frequency * position + u_time); | |
| // Move the position along the normal and transform it | |
| vec3 newPosition = position + normal * displacement; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); | |
| } | |
| </script> | |
| <script id="planeFS" type="shader"> | |
| precision mediump float; | |
| void main() { | |
| gl_FragColor = vec4(0.0, 0.0, 0.8, 1.0); | |
| } | |
| </script> |
| class App { | |
| /** | |
| * @constructor | |
| */ | |
| constructor() { | |
| this.width = window.innerWidth; | |
| this.height = window.innerHeight; | |
| this.DELTA_TIME = 0; | |
| this.LAST_TIME = Date.now(); | |
| this.scene = new Scene(this.width, this.height); | |
| this.plane = new Plane(); | |
| this.scene.add(this.plane.mesh); | |
| const root = document.body.querySelector('.app'); | |
| root.appendChild(this.scene.renderer.domElement); | |
| this.addListeners(); | |
| } | |
| /** | |
| * @method | |
| * @name onResize | |
| * @description Triggered when window is resized | |
| */ | |
| onResize() { | |
| this.width = window.innerWidth; | |
| this.height = window.innerHeight; | |
| this.scene.resize(this.width, this.height); | |
| } | |
| /** | |
| * @method | |
| * @name addListeners | |
| */ | |
| addListeners() { | |
| window.addEventListener('resize', this.onResize.bind(this)); | |
| TweenMax.ticker.addEventListener('tick', this.update.bind(this)); | |
| } | |
| /** | |
| * @method | |
| * @name update | |
| * @description Triggered on every TweenMax tick | |
| */ | |
| update() { | |
| this.DELTA_TIME = Date.now() - this.LAST_TIME; | |
| this.LAST_TIME = Date.now(); | |
| this.plane.update(this.DELTA_TIME); | |
| this.scene.render(); | |
| } | |
| } | |
| class Plane { | |
| /** | |
| * @constructor | |
| */ | |
| constructor() { | |
| this.size = 1000; | |
| this.segments = 200; | |
| this.options = new Options(); | |
| this.options.initGUI(); | |
| this.uniforms = { | |
| u_amplitude: {type: '1f', value: this.options.amplitude}, | |
| u_frequency: {type: '1f', value: this.options.frequency}, | |
| u_time: {type: '1f', value: 0} | |
| } | |
| this.geometry = new THREE.PlaneGeometry(this.size, this.size, this.segments, this.segments); | |
| this.material = new THREE.ShaderMaterial({ | |
| uniforms: this.uniforms, | |
| vertexShader: document.getElementById('planeVS').innerHTML, | |
| fragmentShader: document.getElementById('planeFS').innerHTML, | |
| wireframe: true, | |
| side: THREE.DoubleSide | |
| }); | |
| this.mesh = new THREE.Mesh(this.geometry, this.material); | |
| this.mesh.rotation.x = 360; | |
| } | |
| /** | |
| * @method | |
| * @name update | |
| * @description Triggered on every TweenMax tick | |
| * @param {number} dt - DELTA_TIME | |
| */ | |
| update(dt) { | |
| this.uniforms.u_amplitude.value = this.options.amplitude; | |
| this.uniforms.u_frequency.value = this.options.frequency; | |
| this.uniforms.u_time.value += dt / 1000; | |
| } | |
| } | |
| class Options { | |
| /** | |
| * @constructor | |
| */ | |
| constructor() { | |
| this.amplitude = 10.0; | |
| this.frequency = 0.05; | |
| this.gui = new dat.GUI(); | |
| } | |
| /** | |
| * @method | |
| * @name initGUI | |
| * @description Initialize the dat-gui | |
| */ | |
| initGUI() { | |
| this.gui.close(); | |
| this.gui.add(this, 'amplitude', 1.0, 15.0); | |
| this.gui.add(this, 'frequency', 0.01, 0.1); | |
| } | |
| } | |
| class Scene { | |
| /** | |
| * @constructor | |
| */ | |
| constructor(width, height) { | |
| this.scene = new THREE.Scene(); | |
| this.renderer = new THREE.WebGLRenderer({antialias: true}); | |
| this.renderer.setSize(width, height); | |
| this.renderer.setClearColor(0x000000); | |
| this.camera = new THREE.PerspectiveCamera(50, width / height, 1, 2000); | |
| this.camera.position.z = 100; | |
| this.controls = new THREE.OrbitControls(this.camera); | |
| } | |
| /** | |
| * @method | |
| * @name add | |
| * @description Add a child to the scene | |
| * @param {object} child - A THREE object | |
| */ | |
| add(child) { | |
| this.scene.add(child); | |
| } | |
| /** | |
| * @method | |
| * @name remove | |
| * @description Remove a child from the scene | |
| * @param {object} child - A THREE object | |
| */ | |
| remove(child) { | |
| this.scene.remove(child); | |
| } | |
| /** | |
| * @method | |
| * @name render | |
| * @description Renders/Draw the scene | |
| */ | |
| render() { | |
| this.renderer.autoClearColor = true; | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| /** | |
| * @method | |
| * @name resize | |
| * @description Resize the scene according to screen size | |
| * @param {number} newWidth | |
| * @param {number} newHeight | |
| */ | |
| resize(newWidth, newHeight) { | |
| this.camera.aspect = newWidth / newHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(newWidth, newHeight); | |
| } | |
| } | |
| new App(); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.4/TweenMax.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r77/three.min.js"></script> | |
| <script src="http://threejs.org/examples/js/controls/OrbitControls.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script> |
| html, body | |
| width 100% | |
| height 100% | |
| margin 0 | |
| padding 0 | |
| overflow hidden | |
| background url('http://lab.arnaudrocca.fr/water-shader/assets/images/preview.png') no-repeat center center fixed | |
| background-size cover | |
| font-size 10px | |
| canvas | |
| position absolute | |
| display block | |
| cursor move | |
| .infos | |
| position absolute | |
| margin 0 | |
| bottom 10px | |
| right 10px | |
| color #888 | |
| font-family sans-serif | |
| font-weight 400 | |
| font-size 1rem | |
| line-height .5rem | |
| a | |
| text-decoration none | |
| color #03C | |
| opacity .7 | |
| transition .3s | |
| &:hover | |
| opacity 1 |
Water shader using Three.js, Orbit Controls & TweenMax.
A Pen by Arnaud Rocca on CodePen.