In this tutorial, we create a volume effect by overlapping several images taken at different light direction.
Fix your camera on a tripod and take a photo of your scene with regular lighting.
Without moving the camera, use another source of light such as your phone flashlight, and take 4 additional shots from the same position but changing the direction of the light for each photo.
Take 4 identical photo with light source coming from north, east, west, east, as well as a 5th photo with ambient lighting.
The magic essentially occurs in your shader:
float cheapLuma ( vec3 rgb ) {
return ( rgb.r + rgb.r + rgb.b + rgb.g + rgb.g + rgb.g ) / 6.0;
}
void main () {
vec4 pixelTop = texture2D(imageTop, vUv);
float lTop = cheapLuma( pixelTop.rgb );
vec4 pixelBottom = texture2D(imageBottom, vUv);
float lBottom = cheapLuma( pixelBottom.rgb );
vec4 pixelRight = texture2D(imageRight, vUv);
float lRight = cheapLuma( pixelRight.rgb );
vec4 pixelLeft = texture2D(imageLeft, vUv);
float lLeft = cheapLuma( pixelLeft.rgb );
float distX = (1.0-lLeft + lRight)/2.0;
float distY = (1.0-lBottom + lTop)/2.0;
gl_FragColor = vec4( distX, distY, 1.0, 1.0 );
}
In this example two files are needed:
Main.js
the main class that sets up Three.js scene, camera and renderer.RTT.js
a render texture class for merging the textures into its own buffer for better reusability.import THREE from 'three';
import RTT from './RTT';
const frameSize = { x:512, y:512 };
export default class Main {
constructor () {
this.loader = new THREE.TextureLoader();
this.mousePosition = new THREE.Vector2();
this.clock = new THREE.Clock();
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 );
this.camera.position.z = 1000;
this.renderer = new THREE.WebGLRenderer({alpha:true});
this.renderer.setSize( window.innerWidth, window.innerHeight );
this.renderer.setClearColor( 0x000000, 0 );
document.body.appendChild( this.renderer.domElement );
this.rtt = new RTT(512, 512, this.mousePosition);
// this.rtt.render( this.renderer, this.clock.getDelta());
this.mesh = this.newMesh();
this.scene.add( this.mesh );
// let ambientLight = new THREE.AmbientLight( 0x888888 );
let ambientLight = new THREE.AmbientLight( 0xcccccc );
this.scene.add( ambientLight );
this.light = new THREE.PointLight( 0x777777, 0.7, 2500 );
this.scene.add( this.light );
this.light.position.z = 1500;
document.addEventListener( 'mousemove', this.onMouseMove.bind(this), false );
window.addEventListener( 'resize', this.onWindowResize.bind(this), false );
this.onWindowResize();
this.onMouseMove( {clientX:window.innerWidth/2, clientY:window.innerHeight/2} );
this.animate();
}
onMouseMove(event) {
this.mousePosition.x = (( event.clientX + document.body.scrollLeft ) / window.innerWidth - .5 ) * 2;
this.mousePosition.y = -(( event.clientY + document.body.scrollTop ) / window.innerHeight - .5 ) * 2;
this.light.position.x = window.innerWidth/2 * this.mousePosition.x;
this.light.position.y = window.innerHeight/2 * this.mousePosition.y;
this.light.position.z = this.camera.position.z/2;
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize( window.innerWidth, window.innerHeight );
let scaleWidth = window.innerWidth/frameSize.x;
let scaleHeight = window.innerHeight/frameSize.y;
if ( scaleWidth < scaleHeight )
this.camera.position.z = frameSize.x / this.camera.aspect / (2 * Math.tan(this.camera.fov / 2 * (Math.PI / 180)));
else
this.camera.position.z = frameSize.y / (2 * Math.tan(this.camera.fov / 2 * (Math.PI / 180)));
}
newMesh () {
this.paperTexture = this.loader.load('img/colors-center-512-neon-2.png');
let geometry = new THREE.PlaneBufferGeometry( frameSize.x, frameSize.y );
let material = new THREE.MeshPhongMaterial( {
map: this.paperTexture,
shininess: 5,
specular: 0x222222,
specularMap: this.rtt.texture.texture,
normalMap: this.rtt.texture.texture,
normalScale: new THREE.Vector2( 1, 1 ),
side: THREE.DoubleSide
});
return new THREE.Mesh( geometry, material );
}
animate() {
requestAnimationFrame( this.animate.bind(this) );
this.rtt.render( this.renderer, this.clock.getDelta());
this.renderer.render( this.scene, this.camera );
}
};
Then you need to setup the render buffer that merges the photos:
import THREE from 'three';
const vertexShader = `
precision mediump float;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const fragmentShader = `
precision mediump float;
uniform float time;
uniform sampler2D imageTop;
uniform sampler2D imageBottom;
uniform sampler2D imageRight;
uniform sampler2D imageLeft;
uniform vec3 color;
varying vec2 vUv;
float cheapLuma ( vec3 rgb ) {
return ( rgb.r + rgb.r + rgb.b + rgb.g + rgb.g + rgb.g ) / 6.0;
}
void main () {
vec4 pixelTop = texture2D(imageTop, vUv);
float lTop = cheapLuma( pixelTop.rgb );
vec4 pixelBottom = texture2D(imageBottom, vUv);
float lBottom = cheapLuma( pixelBottom.rgb );
vec4 pixelRight = texture2D(imageRight, vUv);
float lRight = cheapLuma( pixelRight.rgb );
vec4 pixelLeft = texture2D(imageLeft, vUv);
float lLeft = cheapLuma( pixelLeft.rgb );
float distX = (1.0-lLeft + lRight)/2.0;
float distY = (1.0-lBottom + lTop)/2.0;
gl_FragColor = vec4( distX, distY, 1.0, 1.0 );
}
`;
export default class RTT {
constructor (width, height, mousePosition) {
this.loader = new THREE.TextureLoader();
this.lifetime = 0;
this.camera = new THREE.OrthographicCamera(
width / -2, width / 2,
height / 2, height / - 2, -10000, 10000 );
this.scene = new THREE.Scene();
this.texture = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat
});
let geometry = new THREE.PlaneBufferGeometry( width, height );
let material = new THREE.ShaderMaterial( {
uniforms: {
imageTop : { type: "t", value: this.loader.load('img/colors-top-512.png') },
imageBottom : { type: "t", value: this.loader.load('img/colors-bottom-512.png') },
imageRight : { type: "t", value: this.loader.load('img/colors-right-512.png') },
imageLeft : { type: "t", value: this.loader.load('img/colors-left-512.png') },
lightPos : { type: "2f", value: mousePosition },
time : { type: "f", value: 0 },
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
blending: THREE.AdditiveBlending
});
this.quad = new THREE.Mesh( geometry, material );
this.scene.add( this.quad );
}
render (renderer, delta) {
this.lifetime += delta*3;
this.quad.material.uniforms.time.value = this.lifetime;
renderer.render( this.scene, this.camera, this.texture, true );
}
}
You can try various textures such as paper folds...
... or a sand garden ...
Checkout this old github repo for more references.