Эта статья является частью серии статей о three.js. Первая была об основах. Если вы её еще не читали, советую вам сделать это. Предыдущая статья была о текстурах.
Давайте рассмотрим, как использовать различные виды освещения в three.js.
Начинем с одного из наших предыдущих примеров, давайте обновим камеру. Мы установим поле зрения (fov) на 45 градусов, дальнюю плоскость (far) на 100 единиц, и мы переместим камеру на 10 единиц вверх и на 20 единиц назад от начала координат.
*const fov = 45; const aspect = 2; // значение по умолчанию для холста const near = 0.1; *const far = 100; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); +camera.position.set(0, 10, 20);
Далее давайте добавим OrbitControls
. OrbitControls
позволить пользователю вращать
или поворачивать камеру вокруг некоторой точки. OrbitControls
- это
дополнительные функции three.js, поэтому сначала нам нужно
включить их в нашу страницу.
import * as THREE from '/build/three.module.js'; +import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
Теперь мы можем использовать их. Мы передаем в OrbitControls
камеру для
управления и элемент DOM для получения входных событий
const controls = new OrbitControls(camera, canvas); controls.target.set(0, 5, 0); controls.update();
Мы также устанавливаем target на 5 орбит вокруг источника, а затем вызываем
controls.update
чтобы элементы управления использовали новую цель.
Далее давайте сделаем некоторые вещи, чтобы включить освещение. Сначала мы сделаем плоскость земли. Мы применим крошечную текстуру шахматной доски размером 2x2, которая выглядит следующим образом
Сначала мы загружаем текстуру, устанавливаем фильтрацию nearest и устанавливаем число повторений. Поскольку текстура представляет собой шахматную доску размером 2x2 пикселя, при повторении и установке повторения равным половине размера плоскости каждая клетка на шахматной доске будет иметь размер ровно 1 единицу;
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; const repeats = planeSize / 2; texture.repeat.set(repeats, repeats);
Затем мы создаем геометрию плоскости, материал для плоскости и сетку, чтобы вставить ее в сцену. Плоскости по умолчанию находятся в плоскости XY, но земля находится в плоскости XZ, поэтому мы вращаем ее.
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);
Давайте добавим куб и сферу, чтобы у нас было 3 вещи для освещения, включая плоскость
{ 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); }
Теперь, когда у нас есть сцена для освещения, давайте добавим свет!
AmbientLight
Сначала давайте сделаем AmbientLight
const color = 0xFFFFFF; const intensity = 1; const light = new THREE.AmbientLight(color, intensity); scene.add(light);
Давайте также сделаем так, чтобы мы могли регулировать параметры света.
Мы снова будем использовать lil-gui.
Чтобы иметь возможность настроить цвет с помощью lil-gui, нам нужен небольшой
помощник, который представляет свойство для lil-gui, которое выглядит как
шестнадцатеричная цветовая строка CSS (например: #FF8844
).
Наш helper получит цвет из именованного свойства, преобразует его в
шестнадцатеричную строку, чтобы предложить lil-gui. Когда lil-gui
попытается установить свойство helper'а, мы присвоим результат
обратно цвету источника света.
Вот helper:
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); } }
И вот наш код настройки lil-gui
const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); gui.add(light, 'intensity', 0, 2, 0.01);
И вот результат
Нажмите и перетащите сценy, чтобы вращать камеру.
Обратите внимание, формы плоские. AmbientLight
только умножил цвет материала
на цвет света с учетом интенсивности.
color = materialColor * light.color * light.intensity;
Вот и все. У него нет направления. Этот стиль окружающего (ambient) освещения на самом деле не так полезен, как освещение, так как он на 100% даже за исключением изменения цвета всего на сцене, не очень похож на освещение. Что помогает, так это делает темные не слишком темными.
HemisphereLight
Давайте переключим код на HemisphereLight
. HemisphereLight
принимает цвет неба и основной цвет и просто умножает цвет материала
между этими двумя цветами. Цвет неба, если поверхность объекта направлена
вверх, и цвет земли, если поверхность объекта направлена вниз.
Вот новый код
-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);
Давайте также обновим код lil-gui для редактирования обоих цветов.
const gui = new GUI(); -gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); +gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('skyColor'); +gui.addColor(new ColorGUIHelper(light, 'groundColor'), 'value').name('groundColor'); gui.add(light, 'intensity', 0, 2, 0.01);
Результат:
Еще раз обратите внимание, что объема почти нет, все выглядит плоско.
HemisphereLight
используется в сочетании с другим светом и может
помочь дать хороший вид влияния цвета неба и земли.
Таким образом, его лучше всего использовать в сочетании с другим источником
света или заменой AmbientLight
.
DirectionalLight
Давайте переключим код на DirectionalLight
.
DirectionalLight
часто используется для воспроизведения солнца.
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);
Обратите внимание, что мы должны были добавить light
и light.target
к сцене. DirectionalLight
будет светить в направлении к своей цели.
Давайте сделаем так, чтобы мы могли перемещать цель, добавляя ее в наш графический интерфейс.
const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); gui.add(light, 'intensity', 0, 2, 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);
Трудно понять, что происходит. Three.js имеет несколько вспомогательных
объектов, которые мы можем добавить к нашей сцене, чтобы помочь
визуализировать невидимые части сцены. В этом случае мы будем
использовать тот DirectionalLightHelper
, который нарисует плоскость,
чтобы изобразить источник света, и линию от света к цели.
Мы просто передаем ему свет и добавляем его на сцену.
const helper = new THREE.DirectionalLightHelper(light); scene.add(helper);
Пока мы работаем с ним, давайте сделаем так, чтобы мы могли установить
как положение источника света, так и цели. Для этого мы сделаем функцию,
которая по заданному Vector3
скорректирует его x
, y
и z
свойства,
используя 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(); }
Обратите внимание, что нам нужно вызывать update
функцию помощника
каждый раз, когда мы что-то меняем, чтобы помощник знал, что нужно
обновить себя. Таким образом, мы передаем onChangeFn
функцию
для вызова в любое время, а lil-gui обновляет значение.
Затем мы можем использовать это как для положения источника света, так и для цели, как тут
+function updateLight() { + light.target.updateMatrixWorld(); + helper.update(); +} +updateLight(); const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); gui.add(light, 'intensity', 0, 2, 0.01); +makeXYZGUI(gui, light.position, 'position', updateLight); +makeXYZGUI(gui, light.target.position, 'target', updateLight);
Теперь мы можем переместить свет и его цель
Вращайтесь по орбите камеры, и это станет легче видеть. Плоскость представляет собой
DirectionalLight
потому что направленный свет вычисляет свет
поступающий в одном направлении. Нет точки откуда исходит свет,
это бесконечная плоскость света, излучающая параллельные лучи света.
PointLight
PointLight
- это свет, который сидит в какой-то точке и излучает свет
во всех направлениях от этой точки. Давайте изменим код.
const color = 0xFFFFFF; const intensity = 1; -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);
Давайте также перейдем к PointLightHelper
-const helper = new THREE.DirectionalLightHelper(light); +const helper = new THREE.PointLightHelper(light); scene.add(helper);
и поскольку нет цели, то onChange
функция может быть проще.
function updateLight() { - light.target.updateMatrixWorld(); helper.update(); } -updateLight();
Обратите внимание, что на каком-то уровне PointLightHelper
не имеет точки.
Он просто рисует маленький каркас ромба. Это может быть любая
форма, которую вы хотите, просто добавьте mesh к самому источнику света.
PointLight
имеет дополнительное свойство distance
.
Если distance
= 0, то PointLight
светит до бесконечности. Если значение
distance
больше 0, то свет излучает свою полную интенсивность и
исчезает, с увеличением distance
вдали от света.
Давайте настроим графический интерфейс, чтобы мы могли регулировать расстояние.
const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); gui.add(light, 'intensity', 0, 2, 0.01); +gui.add(light, 'distance', 0, 40).onChange(updateLight); makeXYZGUI(gui, light.position, 'position', updateLight); -makeXYZGUI(gui, light.target.position, 'target', updateLight);
А теперь попробуйте.
Обратите внимание, когда distance
> 0, как свет гаснет.
SpotLight
Прожекторы - это точечный источник света с прикрепленным к нему конусом, который светит только внутри конуса. Там на самом деле 2 конуса. Внешний конус и внутренний конус. Между внутренним и внешним конусом свет исчезает от полной интенсивности до нуля.
Чтобы использовать SpotLight
нам нужна цель, т.к. это направленный свет.
Конус света открываем по направлению к цели.
Модификация нашего DirectionalLight
с помощником сверху
const color = 0xFFFFFF; const intensity = 1; -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);
Угол конуса прожектора задается с помощью свойства angle
в радианах. Мы будем использовать наш DegRadHelper
из
статьи про текстуры для представления пользовательского интерфейса в градусах..
gui.add(new DegRadHelper(light, 'angle'), 'value', 0, 90).name('angle').onChange(updateLight);
Внутренний конус определяется путем установки свойства penumbra
в процентах от внешнего конуса. Другими словами, когда penumbra
= 0 , то внутренний код
имеет такой же размер (0 = нет разницы) от внешнего конуса. Когда значение
penumbra
равно 1, свет гаснет, начиная с центра конуса до внешнего конуса.
Когда penumbra
равно .5, то свет гаснет, начиная с 50% между центром внешнего конуса.
gui.add(light, 'penumbra', 0, 1, 0.01);
Обратите внимание, что при значении по умолчанию penumbra
= 0 и прожектор
имеет очень резкий край. По мере того, как вы наращиваете penumbra
к 1
края размываются.
Может быть трудно увидеть конус прожектора. Причина в том, что он ниже земли. Сократите расстояние до 5, и вы увидите открытый конец конуса.
RectAreaLight
Есть еще один тип света - RectAreaLight
, который представляет именно то,
на что это похоже, прямоугольную область света, такую как длинный флуоресцентный
свет или, возможно, матовый небесный свет в потолке.
RectAreaLight
работает только с MeshStandardMaterai
и
MeshPhysicalMaterial
поэтому давайте изменим все наши материалы на 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); }
Для использования RectAreaLight
нам нужно включить некоторые дополнительные возможности three.js
import * as THREE from '/build/three.module.js'; +import {RectAreaLightUniformsLib} from '/examples/jsm/lights/RectAreaLightUniformsLib.js'; +import {RectAreaLightHelper} from '/examples/jsm/helpers/RectAreaLightHelper.js';
function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({canvas}); + RectAreaLightUniformsLib.init();
Если вы забудете RectAreaLightUniformsLib, индикатор все равно будет работать, но он будет выглядеть забавно, поэтому не забудьте включить дополнительный код.
Теперь мы можем создать свет
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(30); scene.add(light); *const helper = new RectAreaLightHelper(light); scene.add(helper);
Единственное, что следует заметить, в отличие от DirectionalLight
и SpotLight
,
RectAreaLight
не использует цель. Он просто использует свой поворот.
Давайте также настроим графический интерфейс. Мы сделаем так, чтобы мы могли вращать
свет и регулировать его width
и height
const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); 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('x rotation'); gui.add(new DegRadHelper(light.rotation, 'y'), 'value', -180, 180).name('y rotation'); gui.add(new DegRadHelper(light.rotation, 'z'), 'value', -180, 180).name('z rotation'); makeXYZGUI(gui, light.position, 'position');
И вот что.
Одна вещь, которую мы не охватили, это то, что есть настройка для WebGLRenderer
вызываемого в physicallyCorrectLights
. Это влияет на то, как свет падает в зависимости
от расстояния до предмета. Это влияет только на PointLight
и SpotLight
.
RectAreaLight
делает это автоматически.
Для источников света, хотя основная идея заключается в том, что вы не устанавливаете
расстояние для их затухания и не устанавливаете intensity
. Вместо этого вы устанавливаете силу
power
света в люменах, а затем three.js будет использовать физические
вычисления, как у настоящих источников света. Единицами three.js в этом случае являются метры,
а лампочка мощностью 60 Вт будет иметь около 800 люмен.
Там также есть распад decay
, он должен быть установлен в 2
для реализма.
Давайте проверим это.
Сначала мы включим физически правильное освещение
const renderer = new THREE.WebGLRenderer({canvas}); +renderer.physicallyCorrectLights = true;
Затем мы установим power
= 800 lumens, decay
= 2 и distance
до Infinity
.
const color = 0xFFFFFF; const intensity = 1; const light = new THREE.PointLight(color, intensity); light.power = 800; light.decay = 2; light.distance = Infinity;
и мы добавим графический интерфейс, чтобы мы могли изменить power
и decay
const gui = new GUI(); gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color'); gui.add(light, 'decay', 0, 4, 0.01); gui.add(light, 'power', 0, 2000);
Важно отметить, что каждый источник света, который вы добавляете в сцену, замедляет скорость рендеринга сцены в three.js, поэтому вы всегда должны стараться использовать как можно меньше для достижения своих целей.
Далее давайте перейдем к работе с камерами.