An implementation of Kenny Mitchell's Volumetric Light Scattering as a Post-Process from GPU Gems 3, using three.js
https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch13.html
A Pen by Andrew Berg on CodePen.
| THREE.VolumetericLightShader = { | |
| uniforms: { | |
| tDiffuse: {value:null}, | |
| lightPosition: {value: new THREE.Vector2(0.5, 0.5)}, | |
| exposure: {value: 0.18}, | |
| decay: {value: 0.95}, | |
| density: {value: 0.8}, | |
| weight: {value: 0.4}, | |
| samples: {value: 50} | |
| }, | |
| vertexShader: [ | |
| "varying vec2 vUv;", | |
| "void main() {", | |
| "vUv = uv;", | |
| "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", | |
| "}" | |
| ].join("\n"), | |
| fragmentShader: [ | |
| "varying vec2 vUv;", | |
| "uniform sampler2D tDiffuse;", | |
| "uniform vec2 lightPosition;", | |
| "uniform float exposure;", | |
| "uniform float decay;", | |
| "uniform float density;", | |
| "uniform float weight;", | |
| "uniform int samples;", | |
| "const int MAX_SAMPLES = 100;", | |
| "void main()", | |
| "{", | |
| "vec2 texCoord = vUv;", | |
| "vec2 deltaTextCoord = texCoord - lightPosition;", | |
| "deltaTextCoord *= 1.0 / float(samples) * density;", | |
| "vec4 color = texture2D(tDiffuse, texCoord);", | |
| "float illuminationDecay = 1.0;", | |
| "for(int i=0; i < MAX_SAMPLES; i++)", | |
| "{", | |
| "if(i == samples){", | |
| "break;", | |
| "}", | |
| "texCoord -= deltaTextCoord;", | |
| "vec4 sample = texture2D(tDiffuse, texCoord);", | |
| "sample *= illuminationDecay * weight;", | |
| "color += sample;", | |
| "illuminationDecay *= decay;", | |
| "}", | |
| "gl_FragColor = color * exposure;", | |
| "}" | |
| ].join("\n") | |
| }; | |
| THREE.AdditiveBlendingShader = { | |
| uniforms: { | |
| tDiffuse: { value:null }, | |
| tAdd: { value:null } | |
| }, | |
| vertexShader: [ | |
| "varying vec2 vUv;", | |
| "void main() {", | |
| "vUv = uv;", | |
| "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", | |
| "}" | |
| ].join("\n"), | |
| fragmentShader: [ | |
| "uniform sampler2D tDiffuse;", | |
| "uniform sampler2D tAdd;", | |
| "varying vec2 vUv;", | |
| "void main() {", | |
| "vec4 color = texture2D( tDiffuse, vUv );", | |
| "vec4 add = texture2D( tAdd, vUv );", | |
| "gl_FragColor = color + add;", | |
| "}" | |
| ].join("\n") | |
| }; | |
| THREE.PassThroughShader = { | |
| uniforms: { | |
| tDiffuse: { value: null } | |
| }, | |
| vertexShader: [ | |
| "varying vec2 vUv;", | |
| "void main() {", | |
| "vUv = uv;", | |
| "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", | |
| "}" | |
| ].join( "\n" ), | |
| fragmentShader: [ | |
| "uniform sampler2D tDiffuse;", | |
| "varying vec2 vUv;", | |
| "void main() {", | |
| "gl_FragColor = texture2D( tDiffuse, vec2( vUv.x, vUv.y ) );", | |
| "}" | |
| ].join( "\n" ) | |
| }; | |
| (function(){ | |
| var scene, camera, renderer, composer, box, pointLight, | |
| occlusionComposer, occlusionRenderTarget, occlusionBox, lightSphere, | |
| volumetericLightShaderUniforms, | |
| DEFAULT_LAYER = 0, | |
| OCCLUSION_LAYER = 1, | |
| renderScale = 0.5, | |
| angle = 0, | |
| gui = new dat.GUI(); | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); | |
| renderer = new THREE.WebGLRenderer(); | |
| renderer.setPixelRatio( window.devicePixelRatio ); | |
| renderer.setSize( window.innerWidth, window.innerHeight ); | |
| document.body.appendChild( renderer.domElement ); | |
| function setupScene(){ | |
| var ambientLight, | |
| geometry, | |
| material; | |
| ambientLight = new THREE.AmbientLight(0x2c3e50); | |
| scene.add(ambientLight); | |
| pointLight = new THREE.PointLight(0xffffff); | |
| scene.add(pointLight); | |
| geometry = new THREE.SphereBufferGeometry( 1, 16, 16 ); | |
| material = new THREE.MeshBasicMaterial( { color: 0xffffff } ); | |
| lightSphere = new THREE.Mesh( geometry, material ); | |
| lightSphere.layers.set( OCCLUSION_LAYER ); | |
| scene.add( lightSphere ); | |
| geometry = new THREE.BoxBufferGeometry( 1, 1, 1 ); | |
| material = new THREE.MeshPhongMaterial( { color: 0xe74c3c } ); | |
| box = new THREE.Mesh( geometry, material ); | |
| box.position.z = 2; | |
| scene.add( box ); | |
| material = new THREE.MeshBasicMaterial( { color:0x000000 } ); | |
| occlusionBox = new THREE.Mesh( geometry, material); | |
| occlusionBox.position.z = 2; | |
| occlusionBox.layers.set( OCCLUSION_LAYER ); | |
| scene.add( occlusionBox ); | |
| camera.position.z = 6; | |
| } | |
| function setupPostprocessing(){ | |
| var pass; | |
| occlusionRenderTarget = new THREE.WebGLRenderTarget( window.innerWidth * renderScale, window.innerHeight * renderScale ); | |
| occlusionComposer = new THREE.EffectComposer( renderer, occlusionRenderTarget); | |
| occlusionComposer.addPass( new THREE.RenderPass( scene, camera ) ); | |
| pass = new THREE.ShaderPass( THREE.VolumetericLightShader ); | |
| pass.needsSwap = false; | |
| occlusionComposer.addPass( pass ); | |
| volumetericLightShaderUniforms = pass.uniforms; | |
| composer = new THREE.EffectComposer( renderer ); | |
| composer.addPass( new THREE.RenderPass( scene, camera ) ); | |
| pass = new THREE.ShaderPass( THREE.AdditiveBlendingShader ); | |
| pass.uniforms.tAdd.value = occlusionRenderTarget.texture; | |
| composer.addPass( pass ); | |
| pass.renderToScreen = true; | |
| } | |
| function onFrame(){ | |
| requestAnimationFrame( onFrame ); | |
| update(); | |
| render(); | |
| } | |
| function update(){ | |
| var radius = 2.5, | |
| xpos = Math.sin(angle) * radius, | |
| zpos = Math.cos(angle) * radius; | |
| box.position.set( xpos, 0, zpos); | |
| box.rotation.x += 0.01; | |
| box.rotation.y += 0.01; | |
| occlusionBox.position.copy(box.position); | |
| occlusionBox.rotation.copy(box.rotation); | |
| angle += 0.02; | |
| } | |
| function render(){ | |
| camera.layers.set(OCCLUSION_LAYER); | |
| renderer.setClearColor(0x000000); | |
| occlusionComposer.render(); | |
| camera.layers.set(DEFAULT_LAYER); | |
| renderer.setClearColor(0x090611); | |
| composer.render(); | |
| } | |
| function setupGUI(){ | |
| var folder, | |
| min, | |
| max, | |
| step, | |
| updateShaderLight = function(){ | |
| var p = lightSphere.position.clone(), | |
| vector = p.project(camera), | |
| x = ( vector.x + 1 ) / 2, | |
| y = ( vector.y + 1 ) / 2; | |
| volumetericLightShaderUniforms.lightPosition.value.set(x, y); | |
| pointLight.position.copy(lightSphere.position); | |
| }; | |
| folder = gui.addFolder('Light Position'); | |
| folder.add(lightSphere.position, 'x').min(-10).max(10).step(0.1).onChange(updateShaderLight); | |
| folder.add(lightSphere.position, 'y').min(-10).max(10).step(0.1).onChange(updateShaderLight); | |
| folder.add(lightSphere.position, 'z').min(-10).max(10).step(0.1).onChange(updateShaderLight); | |
| folder.open(); | |
| folder = gui.addFolder('Volumeteric Light Shader'); | |
| Object.keys(volumetericLightShaderUniforms).forEach(function(key) { | |
| if(key !== 'tDiffuse' && key != 'lightPosition' ){ | |
| prop = volumetericLightShaderUniforms[key]; | |
| switch ( key ) { | |
| case 'exposure': | |
| min = 0; | |
| max = 1; | |
| step = 0.01; | |
| break; | |
| case 'decay': | |
| min = 0.8; | |
| max = 1; | |
| step = 0.001; | |
| break; | |
| case 'density': | |
| min = 0; | |
| max = 1; | |
| step = 0.01; | |
| break; | |
| case 'weight': | |
| min = 0; | |
| max = 1; | |
| step = 0.01; | |
| break; | |
| case 'samples': | |
| min = 1; | |
| max = 100; | |
| step = 1.0; | |
| break; | |
| } | |
| folder.add(prop, 'value').min(min).max(max).step(step).name(key); | |
| } | |
| }); | |
| folder.open(); | |
| } | |
| function addRenderTargetImage(){ | |
| var material, | |
| mesh, | |
| folder; | |
| material = new THREE.ShaderMaterial( THREE.PassThroughShader ); | |
| material.uniforms.tDiffuse.value = occlusionRenderTarget.texture; | |
| mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), material ); | |
| composer.passes[1].scene.add( mesh ); | |
| mesh.visible = false; | |
| folder = gui.addFolder('Light Pass Render Image'); | |
| folder.add(mesh, 'visible'); | |
| folder.add({scale:0.5}, 'scale', { Full: 1, Half: 0.5, Quarter: 0.25 }) | |
| .onChange(function(value) { | |
| renderScale = value; | |
| window.dispatchEvent(new Event('resize')); | |
| }); | |
| folder.open(); | |
| } | |
| window.addEventListener( 'resize', function(){ | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize( window.innerWidth, window.innerHeight ); | |
| var pixelRatio = renderer.getPixelRatio(), | |
| newWidth = Math.floor( window.innerWidth / pixelRatio ) || 1, | |
| newHeight = Math.floor( window.innerHeight / pixelRatio ) || 1; | |
| composer.setSize( newWidth, newHeight ); | |
| occlusionComposer.setSize( newWidth * renderScale, newHeight * renderScale ); | |
| }, false ); | |
| setupScene(); | |
| setupPostprocessing(); | |
| setupGUI(); | |
| addRenderTargetImage(); | |
| onFrame(); | |
| }()) |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r78/three.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script> | |
| <script src="https://abberg.github.io/lib/shaders/CopyShader.js"></script> | |
| <script src="https://abberg.github.io/lib/postprocessing/EffectComposer.js"></script> | |
| <script src="https://abberg.github.io/lib/postprocessing/RenderPass.js"></script> | |
| <script src="https://abberg.github.io/lib/postprocessing/ShaderPass.js"></script> |
| body{ | |
| margin: 0; | |
| } | |
| canvas{ | |
| display: block; | |
| } |
An implementation of Kenny Mitchell's Volumetric Light Scattering as a Post-Process from GPU Gems 3, using three.js
https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch13.html
A Pen by Andrew Berg on CodePen.