Lumières

Cet article fait partie d'une série d'articles sur three.js. Le premier article est les bases de three.js. Si vous ne l'avez pas encore lu et que vous débutez avec three.js, vous pourriez envisager de commencer par là, ainsi que l'article sur la configuration de votre environnement. Le l'article précédent portait sur les textures.

Voyons comment utiliser les différents types de lumières dans three.js.

En partant d'un de nos exemples précédents, mettons à jour la caméra. Nous définirons le champ de vision à 45 degrés, le plan lointain à 100 unités, et nous déplacerons la caméra de 10 unités vers le haut et de 20 unités vers l'arrière par rapport à l'origine

*const fov = 45;
const aspect = 2;  // the canvas default
const near = 0.1;
*const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+camera.position.set(0, 10, 20);

Ajoutons ensuite OrbitControls. Les OrbitControls permettent à l'utilisateur de faire tourner ou d'orbiter la caméra autour d'un point. Les OrbitControls sont une fonctionnalité optionnelle de three.js, nous devons donc d'abord les inclure dans notre page

import * as THREE from 'three';
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

Ensuite, nous pouvons les utiliser. Nous passons aux OrbitControls une caméra à contrôler et l'élément DOM à utiliser pour obtenir les événements d'entrée

const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();

Nous définissons également la cible d'orbite à 5 unités au-dessus de l'origine et appelons ensuite controls.update pour que les contrôles utilisent la nouvelle cible.

Voyons ensuite comment créer des éléments à éclairer. D'abord, nous allons créer un plan au sol. Nous appliquerons une petite texture en damier de 2x2 pixels qui ressemble à ceci :

Nous chargeons d'abord la texture, la définissons en mode répétition, définissons le filtrage au plus proche, et définissons le nombre de fois que nous voulons qu'elle se répète. Étant donné que la texture est un damier de 2x2 pixels, en la répétant et en définissant la répétition à la moitié de la taille du plan, chaque case du damier aura exactement 1 unité de taille ;

const planeSize = 40;

const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/checker.png');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.colorSpace = THREE.SRGBColorSpace;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);

Nous créons ensuite une géométrie de plan, un matériau pour le plan et un maillage pour l'insérer dans la scène. Les plans sont par défaut dans le plan XY, mais le sol est dans le plan XZ, nous le faisons donc pivoter.

const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
  map: texture,
  side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -.5;
scene.add(mesh);

Ajoutons un cube et une sphère pour avoir 3 éléments à éclairer, y compris le plan.

{
  const cubeSize = 4;
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
  const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  scene.add(mesh);
}
{
  const sphereRadius = 3;
  const sphereWidthDivisions = 32;
  const sphereHeightDivisions = 16;
  const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
  const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  scene.add(mesh);
}

Maintenant que nous avons une scène à éclairer, ajoutons des lumières !

AmbientLight

Commençons par créer une Lumière Ambiante

const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.AmbientLight(color, intensity);
scene.add(light);

Faisons en sorte de pouvoir également ajuster les paramètres de la lumière. Nous utiliserons de nouveau lil-gui. Pour pouvoir ajuster la couleur via lil-gui, nous avons besoin d'un petit assistant qui présente une propriété à lil-gui qui ressemble à une chaîne de couleur hexadécimale CSS (par ex. : #FF8844). Notre assistant obtiendra la couleur d'une propriété nommée, la convertira en chaîne hexadécimale pour l'offrir à lil-gui. Lorsque lil-gui essaiera de définir la propriété de l'assistant, nous assignerons le résultat à la couleur de la lumière.

Voici l'assistant :

class ColorGUIHelper {
  constructor(object, prop) {
    this.object = object;
    this.prop = prop;
  }
  get value() {
    return `#${this.object[this.prop].getHexString()}`;
  }
  set value(hexString) {
    this.object[this.prop].set(hexString);
  }
}

Et voici notre code de configuration de lil-gui

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur');
gui.add(light, 'intensity', 0, 5, 0.01);

Et voici le résultat

Cliquez et faites glisser dans la scène pour faire orbiter la caméra.

Remarquez qu'il n'y a pas de définition. Les formes sont plates. La Lumière Ambiante multiplie simplement la couleur du matériau par la couleur de la lumière multipliée par l' intensité.

color = materialColor * light.color * light.intensity;

C'est tout. Elle n'a pas de direction. Ce style d'éclairage ambiant n'est pas très utile en tant qu'éclairage car il est uniformément réparti, donc à part changer la couleur de tout dans la scène, il ne ressemble pas beaucoup à un éclairage. Ce qui aide, c'est qu'il rend les zones sombres moins sombres.

HemisphereLight

Passons au code pour une Lumière Hémisphérique. Une Lumière Hémisphérique prend une couleur de ciel et une couleur de sol et multiplie simplement la couleur du matériau entre ces 2 couleurs — la couleur du ciel si la surface de l'objet pointe vers le haut et la couleur du sol si la surface de l'objet pointe vers le bas.

Voici le nouveau code

-const color = 0xFFFFFF;
+const skyColor = 0xB1E1FF;  // light blue
+const groundColor = 0xB97A20;  // brownish orange
const intensity = 1;
-const light = new THREE.AmbientLight(color, intensity);
+const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);

Mettons également à jour le code lil-gui pour éditer les deux couleurs

const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
+gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur du ciel');
+gui.addColor(new ColorGUIHelper(light, 'groundColor'), 'value').name('couleur du sol');
gui.add(light, 'intensity', 0, 5, 0.01);

Le résultat :

Remarquez de nouveau qu'il n'y a presque pas de définition, tout semble un peu plat. La Lumière Hémisphérique utilisée en combinaison avec une autre lumière peut aider à donner une belle influence de la couleur du ciel et du sol. De cette façon, elle est mieux utilisée en combinaison avec une autre lumière ou en substitut d'une Lumière Ambiante.

DirectionalLight

Passons au code pour une Lumière Directionnelle. Une Lumière Directionnelle est souvent utilisée pour représenter le soleil.

const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);

Remarquez que nous avons dû ajouter la light et la light.target à la scène. Une Lumière Directionnelle three.js brillera dans la direction de sa cible.

Faisons en sorte de pouvoir déplacer la cible en l'ajoutant à notre interface GUI.

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur');
gui.add(light, 'intensity', 0, 5, 0.01);
gui.add(light.target.position, 'x', -10, 10);
gui.add(light.target.position, 'z', -10, 10);
gui.add(light.target.position, 'y', 0, 10);

Il est un peu difficile de voir ce qui se passe. Three.js dispose d'un ensemble d'objets d'aide que nous pouvons ajouter à notre scène pour aider à visualiser les parties invisibles d'une scène. Dans ce cas, nous utiliserons le Helper de Lumière Directionnelle qui dessinera un plan, pour représenter la lumière, et une ligne de la lumière à la cible. Nous lui passons simplement la lumière et l'ajoutons à la scène.

const helper = new THREE.DirectionalLightHelper(light);
scene.add(helper);

Pendant que nous y sommes, faisons en sorte de pouvoir définir à la fois la position de la lumière et la cible. Pour ce faire, nous allons créer une fonction qui, étant donné un Vector3, ajustera ses propriétés x, y, et z en utilisant lil-gui.

function makeXYZGUI(gui, vector3, name, onChangeFn) {
  const folder = gui.addFolder(name);
  folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
  folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
  folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
  folder.open();
}

Notez que nous devons appeler la fonction update de l'assistant chaque fois que nous changeons quelque chose afin que l'assistant sache qu'il doit se mettre à jour. Ainsi, nous passons une fonction onChangeFn qui sera appelée chaque fois que lil-gui met à jour une valeur.

Ensuite, nous pouvons l'utiliser à la fois pour la position de la lumière et pour la position de la cible, comme ceci

+function updateLight() {
+  light.target.updateMatrixWorld();
+  helper.update();
+}
+updateLight();

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur');
gui.add(light, 'intensity', 0, 5, 0.01);

+makeXYZGUI(gui, light.position, 'position', updateLight);
+makeXYZGUI(gui, light.target.position, 'cible', updateLight);

Nous pouvons maintenant déplacer la lumière, et sa cible

Faites orbiter la caméra et il devient plus facile de voir. Le plan représente une Lumière Directionnelle car une lumière directionnelle calcule la lumière venant dans une seule direction. Il n'y a pas de point d'où la lumière provient, c'est un plan infini de lumière émettant des rayons parallèles.

PointLight

Une Lumière Ponctuelle est une lumière qui se situe à un point et projette de la lumière dans toutes les directions à partir de ce point. Modifions le code.

const color = 0xFFFFFF;
-const intensity = 1;
+const intensity = 150;
-const light = new THREE.DirectionalLight(color, intensity);
+const light = new THREE.PointLight(color, intensity);
light.position.set(0, 10, 0);
-light.target.position.set(-5, 0, 0);
scene.add(light);
-scene.add(light.target);

Passons également à un Helper de Lumière Ponctuelle

-const helper = new THREE.DirectionalLightHelper(light);
+const helper = new THREE.PointLightHelper(light);
scene.add(helper);

et comme il n'y a pas de cible, la fonction onChange peut être plus simple.

function updateLight() {
-  light.target.updateMatrixWorld();
  helper.update();
}
-updateLight();

Notez qu'à un certain niveau, un Helper de Lumière Ponctuelle n'a pas de... point. Il dessine simplement un petit losange en fil de fer. Cela pourrait tout aussi facilement être n'importe quelle forme que vous souhaitez, il suffit d'ajouter un maillage à la lumière elle-même.

Une Lumière Ponctuelle a la propriété supplémentaire de distance. Si la distance est 0, alors la Lumière Ponctuelle brille à l'infini. Si la distance est supérieure à 0, alors la lumière brille à pleine intensité au niveau de la lumière et s'estompe jusqu'à ne plus avoir d'influence à distance unités de distance de la lumière.

Configurons l'interface GUI pour que nous puissions ajuster la distance.

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur');
gui.add(light, 'intensity', 0, 250, 1);
+gui.add(light, 'distance', 0, 40).onChange(updateLight);

makeXYZGUI(gui, light.position, 'position', updateLight);
-makeXYZGUI(gui, light.target.position, 'target', updateLight);

Et maintenant, essayez.

Remarquez quand distance est > 0 comment la lumière s'estompe.

SpotLight

Les projecteurs sont effectivement une lumière ponctuelle avec un cône attaché où la lumière ne brille qu'à l'intérieur du cône. Il y a en fait 2 cônes. Un cône extérieur et un cône intérieur. Entre le cône intérieur et le cône extérieur, la lumière s'estompe de la pleine intensité à zéro.

Pour utiliser une Projecteur, nous avons besoin d'une cible, tout comme pour la lumière directionnelle. Le cône de la lumière s'ouvrira vers la cible.

En modifiant notre Lumière Directionnelle avec l'assistant d'en haut

const color = 0xFFFFFF;
-const intensity = 1;
+const intensity = 150;
-const light = new THREE.DirectionalLight(color, intensity);
+const light = new THREE.SpotLight(color, intensity);
scene.add(light);
scene.add(light.target);

-const helper = new THREE.DirectionalLightHelper(light);
+const helper = new THREE.SpotLightHelper(light);
scene.add(helper);

L'angle du cône du projecteur est défini avec la propriété angle en radians. Nous utiliserons notre DegRadHelper de l'article sur les textures pour présenter une interface utilisateur en degrés.

gui.add(new DegRadHelper(light, 'angle'), 'value', 0, 90).name('angle').onChange(updateLight);

Le cône intérieur est défini en réglant la propriété pénombre comme un pourcentage à partir du cône extérieur. En d'autres termes, quand penumbra est 0, alors le cône intérieur a la même taille (0 = aucune différence) que le cône extérieur. Quand la penumbra est 1, alors la lumière s'estompe en partant du centre du cône jusqu'au cône extérieur. Quand penumbra est 0,5, alors la lumière s'estompe en partant de 50 % entre le centre du cône extérieur.

gui.add(light, 'penumbra', 0, 1, 0.01);

Remarquez qu'avec la penumbra par défaut de 0, le projecteur a un bord très net, tandis que lorsque vous ajustez la penumbra vers 1, le bord devient flou.

Il peut être difficile de voir le cône du projecteur. La raison est qu'il est en dessous du sol. Raccourcissez la distance à environ 5 et vous verrez l'extrémité ouverte du cône.

RectAreaLight

Il existe un autre type de lumière, la Lumière Rectangulaire, qui représente exactement ce à quoi cela ressemble : une zone rectangulaire de lumière, comme un long néon fluorescent ou peut-être une lucarne dépolie dans un plafond.

La Lumière Rectangulaire ne fonctionne qu'avec les matériaux MeshStandardMaterial et MeshPhysicalMaterial, nous allons donc changer tous nos matériaux en MeshStandardMaterial

  ...

  const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-  const planeMat = new THREE.MeshPhongMaterial({
+  const planeMat = new THREE.MeshStandardMaterial({
    map: texture,
    side: THREE.DoubleSide,
  });
  const mesh = new THREE.Mesh(planeGeo, planeMat);
  mesh.rotation.x = Math.PI * -.5;
  scene.add(mesh);
}
{
  const cubeSize = 4;
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
- const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
+ const cubeMat = new THREE.MeshStandardMaterial({color: '#8AC'});
  const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  scene.add(mesh);
}
{
  const sphereRadius = 3;
  const sphereWidthDivisions = 32;
  const sphereHeightDivisions = 16;
  const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-  const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
+ const sphereMat = new THREE.MeshStandardMaterial({color: '#CA8'});
  const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  scene.add(mesh);
}

Pour utiliser la Lumière Rectangulaire, nous devons inclure des données optionnelles supplémentaires de three.js et nous inclurons le Helper de Lumière Rectangulaire pour nous aider à visualiser la lumière

import * as THREE from 'three';
+import {RectAreaLightUniformsLib} from 'three/addons/lights/RectAreaLightUniformsLib.js';
+import {RectAreaLightHelper} from 'three/addons/helpers/RectAreaLightHelper.js';

et nous devons appeler RectAreaLightUniformsLib.init

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+  RectAreaLightUniformsLib.init();

Si vous oubliez les données, la lumière fonctionnera toujours mais elle aura un aspect étrange, alors n'oubliez pas d'inclure les données supplémentaires.

Nous pouvons maintenant créer la lumière

const color = 0xFFFFFF;
*const intensity = 5;
+const width = 12;
+const height = 4;
*const light = new THREE.RectAreaLight(color, intensity, width, height);
light.position.set(0, 10, 0);
+light.rotation.x = THREE.MathUtils.degToRad(-90);
scene.add(light);

*const helper = new RectAreaLightHelper(light);
*light.add(helper);

Une chose à noter est que, contrairement à la Lumière Directionnelle et au Projecteur, la Lumière Rectangulaire n'utilise pas de cible. Elle utilise simplement sa rotation. Une autre chose à noter est que l'assistant doit être un enfant de la lumière. Il n'est pas un enfant de la scène comme les autres assistants.

Ajustons également l'interface GUI. Nous allons faire en sorte de pouvoir faire pivoter la lumière et ajuster sa width et sa height

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('couleur');
gui.add(light, 'intensity', 0, 10, 0.01);
gui.add(light, 'width', 0, 20);
gui.add(light, 'height', 0, 20);
gui.add(new DegRadHelper(light.rotation, 'x'), 'value', -180, 180).name('rotation x');
gui.add(new DegRadHelper(light.rotation, 'y'), 'value', -180, 180).name('rotation y');
gui.add(new DegRadHelper(light.rotation, 'z'), 'value', -180, 180).name('rotation z');

makeXYZGUI(gui, light.position, 'position');

Et voici cela.

Il est important de noter que chaque lumière que vous ajoutez à la scène ralentit la vitesse de rendu de la scène par three.js, vous devriez donc toujours essayer d'en utiliser le moins possible pour atteindre vos objectifs.

Ensuite, passons à la gestion des caméras.