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.
Main.js
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 - 0.5) *
2;
this.mousePosition.y =
-((event.clientY + document.body.scrollTop) / window.innerHeight - 0.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:
RTT.js
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 …
After deep research and further experimentation, toothpaste turned out to be a fun candidate for this project.
Checkout the github repo for more references. ¡And Voilà!