Один из организаторов митапов для креативных разработчиков в Бельгии Creative Front-end Belgium в двух постах на CodePen рассказывает и показывает, как шаг за шагом сделать туннель из частиц с эффектом движения в нём. К старту курса по Frontend-разработке делимся сокращённым переводом этих статей об анимации, которая, по словам автора, нравится ему больше всего; вы увидите эксперименты с параметрами анимации, поэтому легко поймёте, как адаптировать код для своих нужд, например, для эффекта на сайте или в вашей игре.
Если есть что-то, что мне действительно нравится, то это туннельная анимация. Ниже расскажу, как сделать эту анимацию.
1. Устанавливаем сцену
Я добавил в демо ниже основу инициализации сцены Three.Js.
Холст HTML (canvas).
Немного CSS для приятного глазу отображения.
Рендерер WebGL, сцена, камера и красный куб, чтобы удостовериться, что всё работает.
Не забудьте добавить на страницу библиотеку Three.Js.
Если вы видите куб, это значит, что можно продолжать:
2. Создаём геометрию трубы
Чтобы создать трубу в Three.Js, сначала нам нужен его маршрут, для получения которого воспользуемся конструктором THREE.CatmullRomCurve3(), который позволит создать плавный сплайн из массива вершин. В этой демонстрации я жёстко закодировал массив точек, которые конвертирую в Vector3(). Со своим массивом вершин при помощи упомянутого конструктора вы можете создать свой маршрут:
//Hard coded array of points
var points = [
[0, 2],
[2, 10],
[-1, 15],
[-3, 20],
[0, 25]
];
//Convert the array of points into vertices
for (var i = 0; i < points.length; i++) {
var x = points[i][0];
var y = 0;
var z = points[i][1];
points[i] = new THREE.Vector3(x, y, z);
}
//Create a path from the points
var path = new THREE.CatmullRomCurve3(points);
Получив маршрут, на его основе можно создать трубку туннеля:
//Create the tube geometry from the path
//1st param is the path
//2nd param is the amount of segments we want to make the tube
//3rd param is the radius of the tube
//4th param is the amount of segment along the radius
//5th param specify if we want the tube to be closed or not
var geometry = new THREE.TubeGeometry( path, 64, 2, 8, false );
//Basic red material
var material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
//Create a mesh
var tube = new THREE.Mesh( geometry, material );
//Add tube into the scene
scene.add( tube );
Сейчас вы должны видеть на сцене вращение трубы.
3. Труба туннеля из полигона SVG
В большинстве случаев жёстко кодировать маршрут не хочется. Можно написать функцию генерации случайного множества точек по какому-то также случайному алгоритму. Но в этой демо мы получаем значения из некоего SVG, созданного в Adobe Illustrator. Если вы не хотите кривую Безье, Illustrator экспортирует маршрут как полигон, например так:
<svg viewBox="0 0 346.4 282.4">
<polygon points="68.5,185.5 1,262.5 270.9,281.9 345.5,212.8 178,155.7 240.3,72.3 153.4,0.6 52.6,53.3 "/>
</svg>
Из полигона мы можем вручную конвертировать SVG в массив:
var points = [
[68.5,185.5],
[1,262.5],
[270.9,281.9],
[345.5,212.8],
[178,155.7],
[240.3,72.3],
[153.4,0.6],
[52.6,53.3],
[68.5,185.5]
];
//Do not forget to set the last parameter to True, since we want our tube to be closed
var geometry = new THREE.TubeGeometry( path, 300, 2, 20, true );
Если есть причина, вы можете создать функцию динамического конвертирования.
Слева вы видите полигон. Труба следует за установленными нами точками.
4. Перемещаем камеру в трубе
У нас есть труба, осталась основная часть — анимация. Воспользуемся полезной функцией маршрута — path.getPointAt(t). Эта функция возвращает точку конкретного процента прохождения маршрута. Этот процент — нормализованное значение от нуля до единицы. 0 — первая точка маршрута, 1 — последняя точка. Чтобы расположить камеру вдоль маршрута, применим функцию на каждом кадре:
//Start the percentage at 0
var percentage = 0;
function render(){
//Increase the percentage
percentage += 0.001;
//Get the point at the specific percentage
var p1 = path.getPointAt(percentage%1);
//Place the camera at the point
camera.position.set(p1.x,p1.y,p1.z);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Функция getPointAt принимает только значения 0 и 1, нужно разделить проценты по модулю на единицу, чтобы гарантировать, что значение не выйдет за пределы 1.
Это отлично сработает с позицией, но камера всегда смотрит в одном направлении. Сделаем так, чтобы камера также смотрела на точку вдоль маршрута, но немного дальше. На каждом кадре рассчитаем точку, где стоит камера, а также точку, на которую камера будет смотреть:
var percentage = 0;
function render(){
percentage += 0.001;
var p1 = path.getPointAt(percentage%1);
//Get another point along the path but further
var p2 = path.getPointAt((percentage + 0.01)%1);
camera.position.set(p1.x,p1.y,p1.z);
//Rotate the camera into the orientation of the second point
camera.lookAt(p2);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
Есть несколько вариантов обновления материала. Сейчас мы имеем простой материал, который сторонами смотрит наружу. И даже теперь, когда камера внутри трубы туннеля, нам нужно изменить стороны. И, поскольку на сцене нет освещения, чтобы легко видеть происходящее, мы можем изменить материал на каркасный:
var material = new THREE.MeshBasicMaterial({
color: 0xff0000, //Red color
side : THREE.BackSide, //Reverse the sides
wireframe:true //Display the tube as a wireframe
});
Вуаля! Камера в трубе перемещается.
5. Добавляем освещение
Не стану вдаваться в детали, но покажу, как поставить основное освещение в трубе. Принцип тот же, что и с перемещением камеры: чтобы правильно разместить освещение, будем полагаться на точку, куда смотрит камера.
Сначала создадим PointLight (источник света) и добавим его на сцену:
//Create a point light in our scene
var light = new THREE.PointLight(0xffffff,1, 50);
scene.add(light);
Меняем материал на чувствительный к свету:
var material = new THREE.MeshLambertMaterial({
color: 0xff0000,
side : THREE.BackSide
});
И, наконец, обновляем рендер функции, чтобы переместить свет:
var percentage = 0;
function render(){
percentage += 0.0003;
var p1 = path.getPointAt(percentage%1);
var p2 = path.getPointAt((percentage + 0.02)%1);
camera.position.set(p1.x,p1.y,p1.z);
camera.lookAt(p2);
light.position.set(p2.x, p2.y, p2.z);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
И вот результат:
6. Давайте экспериментировать
На последнем шаге я создал форк и проиграл анимацию с несколькими параметрами, чтобы создать новую. Посмотрите исходник, если интересно.
Для этой демо я установил разные цвета каждой грани. Так получилась мозаика:
А здесь, чтобы сгенерировать маршрут, я играю с положением на оси Y. Таким образом труба изгибается не только в одной плоскости, а в трёх измерениях.
А здесь я создал пять туннелей различного радиуса и цвета. Они также имеют разную прозрачность для хорошей видимости:
Ниже я расскажу, как вместо простой геометрии TubeGeometry() сгенерировать частицы.
Оригинал второй части.
7. Расчёт положения частиц
Чтобы достичь желаемого результата, сгенерируем круги частиц, все вдоль маршрута. Тот же подход используется Three.Js при генерации трубы, с той лишь разницей, что для создания поверхности трубы добавляются грани. Сначала нужно выбрать деталь трубы и её радиус. Деталь устанавливается двумя значениями:
// The amount of circles that will form the tube
var segments = 500;
// The amount of particles that will shape each circle
var circlesDetail = 10;
// The radius of the tube
var radius = 5;
Теперь, когда мы более или менее знаем количество частиц (segments * circlesDetail), рассчитаем трёхгранники Френе. Я не эксперт в этой области, но, насколько понимаю, нам нужно рассчитать трёхгранники Френе для каждого сегмента трубы. Каждый трёхгранник состоит из касательной, нормали и бинормали. Приблизительно эти значения являются значениями поворота для каждого сегмента, а также значениями указывающими, куда смотрит камера.
Если хочется лучше понимать расчёты, посмотрите статью Википедии.
Благодаря Three.Js нам не нужно разбираться в том, как работает код, можно просто воспользоваться встроенной функцией объекта маршрута:
var frames = path.computeFrenetFrames(segments, true);
// True specify if the path is closed or not, in our case it must be
Результат функции — набор из трёх массивов Vector3():
Теперь, имея всё необходимое для каждого сегмента, мы начнём генерировать частицы вдоль сегмента. Мы храним каждую точку частицы как Vector3 в Grometry() так, чтобы позже использовать точки повторно:
// Create an empty Geometry where we will insert the particles
var geometry = new THREE.Geometry();
Теперь частицы размещены в каждом сегменте. Вот для чего мы проходили циклом по всем сегментам. Я объясню, как работает функция. Посмотрите на код ниже, детали вы найдёте в комментариях:
// Loop through all segments
for (var i = 0; i < segments; i++) {
// Get the normal values of the segment from the Frenet frames
var normal = frames.normals[i];
// Get the binormal values of the segment from the Frenet frames
var binormal = frames.binormals[i];
// Calculate the index of the segment (from 0 to 1)
var index = i / segments;
// Get the coordinates of the point in the center of the segment
// We already used the function in the first part to move the camera along the path
var p = path.getPointAt(index);
// Loop for the amount of particles we want along each circle
for (var j = 0; j < circlesDetail; j++) {
// Clone the point in the center of the circle
var position = p.clone();
// We need to position every point based on an angle from 0 to Pi*2
// If you want only half a tube (like a water slide) you could calculate the angle from 0 to Pi.
var angle = (j / circlesDetail) * Math.PI * 2;
// Calculate the sine of the angle
var sin = Math.sin(angle);
// Calculate the negative cosine of the angle
var cos = -Math.cos(angle);
// Calculate the normal of each point based on its angle and the normal and binormal of the segment
var normalPoint = new THREE.Vector3(0,0,0);
normalPoint.x = (cos * normal.x + sin * binormal.x);
normalPoint.y = (cos * normal.y + sin * binormal.y);
normalPoint.z = (cos * normal.z + sin * binormal.z);
// Multiple the normal by the radius so that our tube is not a tube of 1 as radius
normalPoint.multiplyScalar(radius);
// Add the normal values to the center of the circle
position.add(normalPoint);
// Push the vector into our geometry
geometry.vertices.push(position);
}
}
Понять этот код действительно непросто. Я сам смотрел на исходники Three.Js, чтобы написать его. Вы можете посмотреть демонстрацию ниже, чтобы увидеть, как частицы вычисляются одна за другой. Нажмите кнопку Rerun, если труба уже видна полностью.
8. Создаём трубу
У нас есть заполненный вершинами объект Geometry. При помощи конструктора Points, позволяющего рендерить простые точки с прекрасной производительностью, вы можете создать приятные демонстрации с частицами, используя текстуры или различные цвета.
Точно так, как при создании сетки Mesh, нам нужно создать два объекта Points; нужен материал и геометрия. Но геометрию мы уже определили на шаге 5, так что теперь поработаем над материалом:
var material = new THREE.PointsMaterial({
size: 1, // The size of each point
sizeAttenuation: true, // If we want the points to change size depending of distance with camera
color: 0xff0000 // The color of the points
});
9. Добавляем перемещение
Чтобы всё перемещалось, повторно задействуем код из предыдущих демо:
var percentage = 0;
function render() {
// Increase the percentage
percentage += 0.0005;
// Get the point where the camera should go
var p1 = path.getPointAt(percentage % 1);
// Get the point where the camera should look at
var p2 = path.getPointAt((percentage + 0.01) % 1);
camera.position.set(p1.x, p1.y, p1.z);
camera.lookAt(p2);
// Render the scene
renderer.render(scene, camera);
// Animation loop
requestAnimationFrame(render);
}
Простой туннель из частиц готов:
10. Красочный туннель
Здесь я применил отдельный цвет для каждого вектора (Vector в коде), а также Fog на сцене, чтобы добиться эффекта затухания внутри туннеля:
// First create a new color based on the index of the vertice
var color = new THREE.Color("hsl(" + (index * 360 * 4) + ", 100%, 50%)");
// Push the color into the colors array in the Geometry object
geometry.colors.push(color);
var material = new THREE.PointsMaterial({
size: 0.2,
vertexColors: THREE.VertexColors // We specify that the colors must come from the Geometry
});
// Add some fog in the scene
scene.fog = new THREE.Fog(0x000000, 30, 150);
11. Пещера квадратов
Этот туннель сделан из кубов. Вместо объекта Points в каждой позиции вершин я воспользовался объектом Mesh, а применяемые цвета основывались на алгоритме шума Перлина.
12. Октагональный туннель
Здесь я соединяю вершины каждой окружности, чтобы создать линии. Я играю с углом наклона вершин и их цветом, чтобы возникла иллюзия вращения:
or (var i = 0; i < tubeDetail; i++) {
// Create a new geometry for each circle
var circle = new THREE.Geometry();
for (var j = 0; j < circlesDetail; j++) {
// Push the position of the vector
circle.vertices.push(position);
}
// Duplicate the first vector to make sure the circle is closed
circle.vertices.push(circle.vertices[0]);
// Create a new material with a custom color
var material = new THREE.LineBasicMaterial({
color: new THREE.Color("hsl("+(noise.simplex2(index*10,0)*60 + 300)+",50%,50%)")
});
// Create a Line object
var line = new THREE.Line(circle, material);
// Insert into the scene
scene.add(line);
Из относительно простого, как будто плоского гипертекста веб превратился в пространство творчества, экспериментов, впечатлений. Если вы хотите посвятить себя веб-технологиям, то можете присмотреться к нашим курсам по фронтенду или Fullstack-разработке на Python, в рамках которых сможете поработать с HR, чтобы стать веб-разработчиком. Вы также можете узнать, как начать карьеру или вывести свои навыки на новый уровень в других направлениях:
Python, веб-разработка
Data Science и Machine Learning
Мобильная разработка
Java и C#
От основ — в глубину
А также:
Комментарии (2)
Alexey2005
22.08.2021 17:53+3Если уж пошла речь о туннелях на js, то вот самый лучший вариант из всех, что мне известен. Демка всего 1023 байта, причём если страничку целиком сохранить на диск, то работает и запуском локального файла.
Краткое описание, как это вообще работает.
MyraJKee
Впечатляет.