I would like to be able to use normal maps in my site's background images. This CodePen example seems to be the best from all the implementations I've seen so far. It uses three.js to achieve the effect. I'm totally new to three.js, so please forgive my lack of knowledge and high ambitions.
The features still desired are:
① Would like for the light source to be stationary, instead of following the cursor.
The first reason for this is that the mouse is irrelevant for mobile devices. The second reason is that effects that follow a cursor are usually more of a distraction than an enhancement. Instead, having the light source in a fixed position (such as the center of the page), and having the light interact with the background as it is scrolled seems like the best approach.
② Would like to be able to place the background image inside of a smaller div, to include cases when it doesn't need to cover the whole page.
This probably isn't so difficult. It currently appears to be assigned to either the body
element or the whole window
(?). Still examining the code...
③ Would like to be able to define multiple normal map enabled background images, lit by shared lighting (for scene continuity).
If the light position is always relative to the window width
& height
%, maybe this is not so difficult.
④ Would like to be able specify that the image textures be tiled.
Typically this would be done with the background-repeat
CSS property. In three.js it may be different. Looking into it...
Note: Points 5 and 6 that follow are not essential, but I'll mention them anyway in case anyone knows how to implement them.
⑤ Having the ability to light the "scene" (meaning any/all normal map enabled background images on the page) with an image instead of point lights. In other words, HDR image-based lighting. Images would need to be kept low-res so as not to bloat the page, but it could still be considerably better looking than point lights. It appears that three.js does support IBL, but I'm still researching.
⑥ A subtle tilt of the background image position (or light source position) in response to movements detected by the mobile device accelerometer would be cool, but probably difficult to implement, as accelerometer data is no longer accessible by default on iOS. So this one will probably never happen, but... mentioning it for completeness. Alternatively, maybe a parallax motion that reacts to scrolling could provide a similar type of enhancement.
…so please forgive my lack of knowledge and high ambitions.
First, pay attention to the date of this CodePen, it was created in version R71
. A lot has changed since then. Secondly, from what you say, you're starting with Three.js, I'm not sure if starting with custom light implemented in a custom shader is the best way to start your journey with WebGL
... 😉
Your requirements can be created in a number of ways, simpler and more difficult. The CodePen one you presented is one of the (much) more difficult ones.
Enter "multiple" in examples and you will get a series of results showing how what you expect can be achieved.
https://threejs.org/examples/?q=multiple#webgl_multiple_elements
But back to your points...
① Would like for the light source to be stationary, instead of following the cursor.
There is no great methodology here, just turn off listening to mouse movement and position the selected light statically.
② Would like to be able to place the background image inside of a smaller div, to include cases when it doesn't need to cover the whole page.
Modify the canvas size as you need, place it in another div as you want. If you want the sizes to react dynamically (which is desirable in most cases), create a listener for the screen size change. Check example below how insert texture…
③ Would like to be able to define multiple normal map enabled background images, lit by shared lighting (for scene continuity).
You can set the light as shared or individualize it to a specific canvas. In the example you have shared.
④ Would like to be able specify that the image textures be tiled.
In the example you have 4 tiles. Of course, you can divide the viewport however you want, but it is worth spending a little more time to take a closer look at performance. The most adequate solution is probably render scissors. https://threejs.org/docs/#api/en/renderers/WebGLRenderer.setScissor
Generally, if you care about multiplicity, you can have, for example, one canvas and several scenes on it, or you can have a separate canvas for each scene. Everything will come down to the individual project and adapting the relationship between efficiency and the expected effect.
⑤ …In other words, HDR image-based lighting…
⑥ A subtle tilt of the background image position (or light source position) in response to movements detected by the mobile device accelerometer would be cool…
About Sensors and Parallax take look here and here.
Example
body { margin: 0; overflow: hidden; }
.background-div { position: absolute; width: 50vw; height: 50vh; }
#background1 { top: 0; left: 0; }
#background2 { top: 0; left: 50vw; }
#background3 { top: 50vh; left: 0; }
#background4 { top: 50vh; left: 50vw; }
<div class="background-div" id="background1"></div>
<div class="background-div" id="background2"></div>
<div class="background-div" id="background3"></div>
<div class="background-div" id="background4"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.166.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.166.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
function fourPlanes(container) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.z = 20;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enabled = true;
const light = new THREE.PointLight(0xFFFF00, 777, 100);
light.position.set(40, 0, 10);
scene.add(light);
function createTexturedPlane(textureURL, normalMapURL, position, scene) {
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(textureURL);
const normalMap = textureLoader.load(normalMapURL);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
normalMap.wrapS = normalMap.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
normalMap.repeat.set(4, 4);
const material = new THREE.MeshStandardMaterial({
map: texture,
normalMap: normalMap,
});
const geometry = new THREE.PlaneGeometry(10, 10);
const plane = new THREE.Mesh(geometry, material);
plane.position.copy(position);
scene.add(plane);
}
createTexturedPlane('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/water.jpg', 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/waternormals.jpg', new THREE.Vector3(0, 0, 0), scene);
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
}
const background1 = document.getElementById('background1');
const background2 = document.getElementById('background2');
const background3 = document.getElementById('background3');
const background4 = document.getElementById('background4');
fourPlanes(background1);
fourPlanes(background2);
fourPlanes(background3);
fourPlanes(background4);
</script>