Красивый проект с трёхмерной графикой в браузере

Красивый проект с трёхмерной графикой в браузере

Вращаем Луну вокруг Земли

Однажды мы делали простой браузерный проект со звёздами: как будто мы летим сквозь космос, а звёзды пролетают мимо. Сегодня сделаем красивее: нарисуем Луну, которая вращается вокруг Земли и отбрасывает на неё тень.

Красивый проект с трёхмерной графикой в браузере

Что понадобится

Если бы мы программировали этот проект с нуля, то нам бы пришлось писать много кода:

  • создавать и описывать виртуальные источники света;
  • учить объекты правильно реагировать на освещение;
  • рассчитывать тени на объектах;
  • программировать движение объектов;
  • обрабатывать реакции на зум и вращение мышью;
  • и продумывать ещё десяток разных мелочей.

Вместо этого мы возьмём библиотеку LUME, которая умеет всё это делать сама, а нам остаётся задать параметры объектов и указать, что они там делают. После этого LUME всё отрисует на WebGL — специальной библиотеке трёхмерной графики для веба. Мы уже работали с её начальной версией, когда делали цветной арканоид на JavaScript

❤️ Обратите внимание: благодаря распространению веба в него приходит всё больше технологий. Сначала пришёл звук и видео, потом полноценное программирование, теперь — трёхмерная графика. И всё это работает в браузере, без установки дополнительного софта. Вот она, силушка веба! Поэтому мы рекомендуем начинать именно с него.

Как работают сцены в движках трёхмерной графики

Сцена — это то, что видит зритель после запуска кода. Обычно сцена состоит:

  • из камеры — с какой точки зритель увидит всю картину;
  • источника света — у него есть яркость, направление и удалённость от кадра;
  • объектов, которые видит зритель.

Задача движка — обработать свет от источника так, чтобы он правильно всё осветил, тени упали куда нужно и чтобы всё двигалось так, как задумано. Если мы сменим положение камеры, то движок всё пересчитает на лету. 

Основной элемент трёхмерной графики — это ассеты и текстуры. Про них поговорим ниже, когда будем рассказывать про звёздное небо.

В итоге всё работает так:

  1. Мы описываем источник света, камеры и объекты.
  2. Настраиваем, что они делают и как взаимодействуют друг с другом.
  3. Движок всё это пересчитывает и показывает нам.
  4. В итоге мы можем посмотреть всю сцену целиком, приближаться к объектам и удаляться от них и поворачивать сцену мышкой под любым углом:

Красивый проект с трёхмерной графикой в браузере

Создаём страницу и настраиваем стили

Нам понадобится наш обычный HTML-шаблон, который мы будем постепенно наполнять жизнью. Ещё у нас пока нет скрипта, но это не мешает нам сразу его подключить:

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Земля и Луна</title>
  <!-- подключаем свои стили -->
  <link rel="stylesheet" href="style.css">

</head>
<body>

	<!-- подключаем наш скрипт -->
	<script  src="script.js"></script>

</body>
</html>

Так как Земля и Луна находятся в космосе, сделаем сразу тёмный фон и укажем, что страница будет занимать всё доступное ей место в браузере. Это единственные настройки стилей, которые нам понадобятся — остальное сделаем на движке:

/* общие настройки страницы */
html,
body {
	background: #222;
	width: 100%;
	height: 100%;
	margin: 0;
}
Пока видим только темноту, потому что мы ничего больше не добавили на страницу

Добавляем звёзды

Добавим звёзды, чтобы создать ощущение космоса. Можно добить тысячи звёзд вручную и по одной, а можно использовать ассеты и текстуры.

Ассеты — это какой-то набор ресурсов, который используется для создания объекта. Например, в движке может быть стандартный ассет «Мяч», в котором есть такие параметры:

  • размер,
  • вес,
  • расцветка,
  • звук удара по мячу,
  • текстура (кожа, ткань, бумага, металл).

Можно взять стандартный ассет «Мяч», указать нужные параметры и сразу использовать его в своей сцене. Нам не нужно будет отрисовывать его с нуля — движок уже про него знает и сделает всё сам. 

Второй важный элемент — текстура. Если совсем по-простому, то текстура — это картинка, которая натягивается на каркас объекта. Если возьмём текстуру «Ткань» и натянем её на виртуальный кубик, то он будет выглядеть как тканевый кубик. Если возьмём текстуру «Дерево», то получим кубик из дерева, а если «Металл» — то металлический. 

Теперь, когда мы это знаем, создадим звёзды так: нарисуем огромный шар и на внутренней стороне разместим текстуру со звёздами. Если мы будем находиться в центре этого шара, то будет казаться, что вокруг нас космос.

Чтобы это сделать, добавим такой код:

<!-- подключаем LUME -->
<script src="https://unpkg.com/lume@0.3.0-alpha.11/dist/global.js"></script>

<!-- по умолчанию сцена занимает всё доступное место, в нашем случае — весь раздел body -->
<lume-scene id="scene" webgl >
	<!-- настраиваем положение камеры -->
	<lume-camera-rig align-point="0.5 0.5 0.5" initial-polar-angle="0" min-distance="90" max-distance="1000" initial-distance="500"></lume-camera-rig>

	<!-- заполняем пространство звёздами -->
	<lume-sphere id="stars" texture="https://assets.codepen.io/191583/galaxy_starfield.png" receive-shadow="false" has="basic-material" sidedness="back" size="4000 4000 4000" align-point="0.5 0.5 0.5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>

	
</lume-scene>

Всё начинается с тега <lume-scene> — он говорит браузеру, что начинается новая сцена, которую нужно отрисовать. После этого мы установили камеру, указали, как далеко или близко можно двигаться внутри сцены, и настроили расстояние от камеры до сцены на старте: initial-distance="500"

После этого мы сделали звёзды. Расскажем подробно про каждый элемент, потому что остальное будем делать точно так же:

<lume-sphere id="stars" ← создали шар и назвали его stars;

texture="https://assets.codepen.io/191583/galaxy_starfield.png" ← показали, откуда взять текстуру;

receive-shadow="false" ← выключили тени, чтобы на звёзды ничего не отбрасывало тень;

has="basic-material" ← указали, что нам нужно просто покрыть всё текстурой без каких-то эффектов;

sidedness="back" ← текстуру размещаем изнутри шара;

size="4000 4000 4000" ← размеры нашего шара по трём осям;

align-point="0.5 0.5 0.5" mount-point="0.5 0.5 0.5" ← помещаем шар по центру сцены;

color="white"> ← указываем цвет звёзд, потому что у нас дырявая текстура и на месте звёзд на самом деле прозрачность, её нужно заполнить каким-то цветом;

</lume-sphere> ← закончили описывать шар.

Единственная проблема — ничего не сработает, если мы обновим страницу в браузере:

Это сообщение означает, что браузер не может обработать наши элементы, потому что мы их не определили и не сказали, что их можно запускать. Исправим это и добавим первую строчку в файле script.js, который мы подключили заранее:

// на старте запускаем LUME
LUME.defineElements();

Теперь всё в порядке и мы видим звёзды:

Включаем свет

Добавим в сцену источник света. Разницы между просто звёздами и звёздами со светом мы не увидим, но это нам пригодится на следующих этапах. Добавляем в сцену такой код:

<!-- добавляем свет от солнца -->
<!-- настраиваем направление света -->
<lume-node align-point="0.5 0.5" size="0 0" rotation="0 -50 0">
	<lume-node align-point="0.5 0.5" size="0 0" rotation="10 0 0">
		<!-- добавляем источник света -->
		<lume-point-light id="light" size="0 0" position="0 0 1800" color="white" intensity="2" distance="10000" shadow-map-width="2048" shadow-map-height="2048" shadow-camera-far="20000"></lume-point-light>
	</lume-node>
</lume-node>

Тут у нас появился новый элемент — lume-node. Он отвечает за объект в сцене: где он находится, что делает, как реагирует на остальное, поэтому мы сначала создаём новый виртуальный объект, а потом добавляем конкретики с помощью вложенных тегов.

Источник света — полноценный объект, поэтому мы описываем его по всем правилам: указываем яркость, расстояние до сцены, направление света, удаление от камеры и остальные параметры.

Рисуем Землю

Нарисуем Землю точно так же, как мы рисовали звёзды: используем шар и текстуры. Нам понадобятся специальные текстуры, которые выглядят примерно так:

Похоже на глобус, но со странными пропорциями по краям. Это сделано специально: если такую текстуру обернуть вокруг шара, то всё станет на свои места с правильными пропорциями.

Ещё нам понадобится карта глубины и кадра отражений. Глубина отвечает за рельеф и объём Земли: что выше, а что ниже. Чем светлее элемент карты, тем выше он приподнимет текстуру в этом месте:

Карта отражений скажет движку, что и как может отражать свет (и на что может падать тень), а что нет. У нас будет простая карта: на сушу будет падать тень от Луны, а на воду — нет.

<!-- Рисуем Землю -->
<lume-node align-point="0.5 0.5" size="0 0 0">

	<!-- включаем вращение Земли -->
	<lume-node rotation="0 180 0">
		<!-- Земля — это шар, поэтому рисуем шар и натягиваем на него текстуры: изображение Земли, карту глубины и карту отражений-->
		<lume-sphere id="earth" 
			texture="https://assets.codepen.io/191583/earthmap1k.jpg" 
			bump-map="https://assets.codepen.io/191583/earthbump1k.jpg" 
			specular-map="https://assets.codepen.io/191583/earthspec1k.jpg" 
			size="120 120 120" mount-point="0.5 0.5 0.5" align-point="0.5 0.5" color="white">
	</lume-node>
</lume-node>

Вот что у нас получилось:

А вот что мы бы получили, если бы не добавили карту глубины — всё плоское и без теней. Надо понимать, что к реальному виду Земли из космоса это не имеет отношения, потому что из космоса Земля как раз выглядит как идеально гладкий шарик — колебания высоты разглядеть крайне сложно. Но мы делаем красоту, поэтому нам можно. 

Модель Земли без карты глубины — всё плоское и рельефа не видно

Рисуем облака

Чтобы Земля выглядела естественно, добавим облака (типа атмосфера). Сделаем это тоже через шар, только слегка прозрачный и с текстурой облаков:

<!-- добавляем облака -->
<lume-sphere id="clouds" texture="https://assets.codepen.io/191583/earthclouds.png" opacity="0.7" size="125 125 125" align-point="0.5 0.5 0.5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>
</lume-sphere>

Добавляем Луну

Последний объект, который нам осталось добавить, — это Луна. Делаем её так же, как остальное: рисуем шар и натягиваем текстуру. Единственное отличие — мы сразу прописываем, как ей нужно будет вращаться вокруг Земли. Так как мы поставили Землю в центр сцены, то для вращения Луны вокруг Земли мы добавим параметр rotation и говорим, что он будет вращаться по оси Z:

<!-- рисуем Луну -->
<lume-node align-point="0.5 0.5" rotation="90 10 0">
	<!-- указываем, как ей вращаться вокруг Земли -->
	<lume-node id="moonRotator" align-point="0.5 0.5" rotation="0 0 110">
		<!-- рисуем круг и добавляем на него текстуру -->
		<lume-sphere texture="https://assets.codepen.io/191583/moon.jpg" position="250" size="5 5 5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>
	</lume-node>
</lume-node>
Видна Луна, но всё пока стоит на месте

Запускаем вращение

У нас на странице всё готово для того, чтобы мы запустили вращение Земли, Луны и облаков. С вращением Земли и облаков вокруг неё всё просто: мы просто задаём вращение по оси Y в зависимости от времени, а остальные координаты не трогаем. Чтобы облака вращались медленнее, чем Земля, сделаем коэффициент умножения в 3 раза меньше:

// вращаем Землю
earth.rotation = (x, y, z, t) => [x, t * 0.01, z];
// и немного медленнее — вращаем облака вокруг Земли
clouds.rotation = (x, y, z, t) => [x, -t * 0.003, z];

А с Луной немного сложнее: у неё не будет вращения по оси Y (потому что Луна всё время повёрнута к Земле одной стороной), а вместо этого она будет вращаться по оси Z вокруг Земли. Чтобы нам не городить код со стартовым углом вращения и скоростью полёта, используем время: мы просто будем каждый раз считать разницу во времени и в зависимости от неё пересчитывать координаты. Звучит сложно, но на деле это несколько строк:

// получаем текущее время
let lastTime = performance.now();
// сдвиг по времени, на старте равен нулю
let dt = 0;
// вращаем Луну
moonRotator.rotation = (x, y, z, time) => {
	// получаем разницу во времени
	dt = time - lastTime;
	// делаем старое время текущим
	lastTime = time;
	// возвращаем новые координаты Луны
	return [x, y, z + dt * 0.01];
};

Собираем всё в один файл и обновляем страницу: всё начало вращаться, Луна летает и отбрасывает тень на Землю:

Красивый проект с трёхмерной графикой в браузере

Посмотреть на Землю в космосе на странице проекта.

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Земля и Луна</title>
  <!-- подключаем свои стили -->
  <link rel="stylesheet" href="style.css">

</head>
<body>
	<!-- подключаем LUME -->
	<script src="https://unpkg.com/lume@0.3.0-alpha.11/dist/global.js"></script>

	<!-- по умолчанию сцена занимает всё доступное место, в нашем случае — весь раздел body -->
	<lume-scene id="scene" webgl >
		<!-- настраиваем положение камеры на старте и в общем -->
		<lume-camera-rig align-point="0.5 0.5 0.5" initial-polar-angle="0" min-distance="90" max-distance="1000" initial-distance="500"></lume-camera-rig>

		<!-- заполняем пространство звёздами -->
		<lume-sphere id="stars" texture="https://assets.codepen.io/191583/galaxy_starfield.png" receive-shadow="false" has="basic-material" sidedness="back" size="4000 4000 4000" align-point="0.5 0.5 0.5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>

		<!-- добавляем свет от солнца -->
		<!-- настраиваем направление света -->
		<lume-node align-point="0.5 0.5" size="0 0" rotation="0 -50 0">
			<lume-node align-point="0.5 0.5" size="0 0" rotation="10 0 0">
				<!-- добавляем источник света -->
				<lume-point-light id="light" size="0 0" position="0 0 1800" color="white" intensity="2" distance="10000" shadow-map-width="2048" shadow-map-height="2048" shadow-camera-far="20000"></lume-point-light>
			</lume-node>
		</lume-node>

		<!-- Рисуем Землю -->
		<lume-node align-point="0.5 0.5" size="0 0 0">

			<!-- включаем вращение Земли -->
			<lume-node rotation="0 180 0">
				<!-- Земля — это шар, поэтому рисуем шар и натягиваем на него текстуры: изображение Земли, карту глубины и карту отражений-->
				<lume-sphere id="earth" 
					texture="https://assets.codepen.io/191583/earthmap1k.jpg" 
					bump-map="https://assets.codepen.io/191583/earthbump1k.jpg" 
					specular-map="https://assets.codepen.io/191583/earthspec1k.jpg" 
					size="120 120 120" mount-point="0.5 0.5 0.5" align-point="0.5 0.5" color="white">

					<!-- добавляем облака -->
					<lume-sphere id="clouds" texture="https://assets.codepen.io/191583/earthclouds.png" opacity="0.7" size="125 125 125" align-point="0.5 0.5 0.5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>
				</lume-sphere>
			</lume-node>

			<!-- рисуем Луну -->
			<lume-node align-point="0.5 0.5" rotation="90 10 0">
				<!-- указываем, как ей вращаться вокруг Земли -->
				<lume-node id="moonRotator" align-point="0.5 0.5" rotation="0 0 110">
					<!-- рисуем круг и добавляем на него текстуру -->
					<lume-sphere texture="https://assets.codepen.io/191583/moon.jpg" position="250" size="5 5 5" mount-point="0.5 0.5 0.5" color="white"></lume-sphere>
				</lume-node>
			</lume-node>

		</lume-node>
	</lume-scene>

	<!-- подключаем наш скрипт -->
	<script  src="script.js"></script>

</body>
</html>

/* общие настройки страницы */
html,
body {
	background: #222;
	width: 100%;
	height: 100%;
	margin: 0;
}

clouds.rotation = (x, y, z, t) => [x, -t * 0.003, z];// на старте запускаем LUME
LUME.defineElements();

// получаем текущее время
let lastTime = performance.now();
// сдвиг по времени, на старте равен нулю
let dt = 0;
// вращаем Луну
moonRotator.rotation = (x, y, z, time) => {
	// получаем разницу во времени
	dt = time - lastTime;
	// делаем старое время текущим
	lastTime = time;
	// возвращаем новые координаты Луны
	return [x, y, z + dt * 0.01];
};

// вращаем Землю
earth.rotation = (x, y, z, t) => [x, t * 0.01, z];
// и немного медленнее — вращаем облака вокруг Земли

Код:

Joe Pea

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
Что означает ошибка KeyError
Что означает ошибка KeyError

Это значит, что используется несуществующий ключ словаря

easy
Как установить базу данных на сервер и начать с ней работать
Как установить базу данных на сервер и начать с ней работать

Храним данные правильно

medium
Добавляем секретный уровень в пинг-понг на JavaScript
Добавляем секретный уровень в пинг-понг на JavaScript

Мини-проект для тех, кому всё ещё просто.

medium
Что означает ошибка OverflowError: math range error
Что означает ошибка OverflowError: math range error

Это ошибка переполнения из-за математических операций

easy
Простейший математический фокус
Простейший математический фокус

Можно использовать для пикапа или на пьяных вечеринках

easy
Бигдата и тепловые карты на примере твитов Байдена и Трампа
Бигдата и тепловые карты на примере твитов Байдена и Трампа

Сразу видно, кто постит сам, а за кого это делает команда

medium
Подсвечиваем манипуляции и пропаганду на любом сайте
Подсвечиваем манипуляции и пропаганду на любом сайте

Береги свой ум.

easy
Делаем страницу «О себе» на Бутстрапе

Если ты можешь сделать страницу о себе, ты можешь сделать всё.

medium
Непобедимый пинг-понг на JavaScript
Непобедимый пинг-понг на JavaScript

Попробуйте продержаться как можно дольше.

easy
Бесконечная заставка с пинг-понгом
Бесконечная заставка с пинг-понгом

Ещё один способ работать с элементами, не зная их id

medium
medium