Поверхность из частиц на Three.js
JavaScript

Поверхность из частиц на Three.js

Для одного из проектов мне нужно было создать поверхность из частиц. В этой статье я расскажу о способе создания такой поверхности с помощью Three.js

Базовые понятия

Прежде, чем начать разработку, рассмотрим некоторые базовые понятия. Более подробно почитать о Three.js вы можете в документации , здесь я лишь кратко раскрою некоторые термины.

Визуализатор

Рендерер, или визуализатор, — это инструмент отображения сцены в браузере. Существуют различные типы визуализаторов: по умолчанию, WebGL, но также можно выбрать Canvas, SVG, CSS или DOM. Они отличаются способом отрисовки, но для пользователя результат должен быть идентичным.

Сцена

Сцена — это место, которое отрисовывается на экране. При создании новых объектов, мы будем добавлять их на сцену, чтобы сделать видимыми на экране.

Камера

Камера — это обязательный элемент сцены, который имеет координаты и угол поворота, а также некоторые специфичные параметры, такие как соотношение сторон, минимальная и максимальная дальность и т.д. В Three.js есть несколько видов камер, нас будет интересовать объект PerspectiveCamera.

Структура HTML-документа

Мы будет использовать следующую структуру HTML-документа:


<!DOCTYPE html>
<html>
  <head>
    <title>Three.js particles demo</title>
    <meta charset="utf-8">
    <style>
      html, body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
      }

      #particles {
        width: 100%;
        height: 100%;
        background-color: #000000;
      }
    </style>
  </head>
  <body>
    <div id="particles"></div>

    <script src="js/jquery-1.11.3.min.js"></script>
    <script src="js/three.min.js"></script>
    <script src="js/particles.js"></script>
  </body>
</html>

В этой структуре я добавила блок с id="particles", подключила jQuery, Three.js и скрипт particles.js со следующим содержимым:


(function(global) {
    "use strict";

    var camera, scene, renderer;

    var Particles = {
        init: function(el) {},

        animate: function() {},

        render: function() {}
    };

    global.Particles = Particles;
})(window);

В этом файле создается объект Particles, с которым мы будем работать дальше, и добавляются заготовки необходимых методов. Метод init будет отвечать за инициализацию сцены. В методе animate с помощью requestAnimationFrame будем отрисовывать сцену, а метод render будет отвечать непосредственно за отрисовку.

Инициализация сцены

Метод init отвечает за инициализацию сцены, принимает один параметр — jQuery-элемент, в котором будет отрисована сцена.


init: function(el) {
    var terrainSize = 200; //размер поверхности

    // Создание сцены
    scene = new THREE.Scene();

    // Создание камеры
    camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000);
    camera.position.set(0, 200, 1700);
    camera.rotation.set(0, 0, 0);

    // Создание частиц
    /* Этот участок кода мы рассмотрим подробнее дальше */
    var geometry = new THREE.Geometry();
    var vertex, i1, j1;
    for (var i = 0; i < terrainSize; i++) {
        for (var j = 0; j < terrainSize; j++) {
            vertex = new THREE.Vector3();
            vertex.x = (i - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
            vertex.y = 125;
            vertex.z = (j - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
            geometry.vertices.push(vertex);
        }
    }

    // Создание текстуры
    var textureLoader = new THREE.TextureLoader();
    var material = new THREE.PointsMaterial({
        color: 0xffdb8f,
        size: 10,
        map: textureLoader.load("particle.png"),
        blending: THREE.AdditiveBlending,
        transparent: true
    });
    // Добавляем систему частиц на сцену
    var particles = new THREE.Points(geometry, material);
    scene.add(particles);

    // Создание визуализатора
    renderer = new THREE.WebGLRenderer({alpha: true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(0x6f267d, 0);
    el.append(renderer.domElement);

    Particles.animate();
}

Изображение, которое использовалось для создания текстуры, можно скачать здесь .

В конце метода init вызывается метод animate, чтобы отрисовать сцену.

Теперь рассмотрим метод animate:


animate: function() {
    requestAnimationFrame(Particles.animate);
    Particles.render();
}

В этом методе с помощью функции requestAnimationFrame при обновлении экрана вызывается метод Particles.render, а также Particles.render вызывается непосредственно.

И наконец, рассмотрим содержимое метода render:


render: function() {
    camera.lookAt(scene.position);
    renderer.render(scene, camera);
}

Здесь все просто. Я направляю камеру в центр сцены и вызываю метод render визуализатора.

Теперь нужно отрисовать сцену на странице, для этого в index.html добавим следующий код:


<script>
      (function($) {
        $(function () {
          Particles.init($('#particles'));
        });
      })(jQuery);
</script>

Результат:

Уже неплохо, но получилась плоскость из частиц. Теперь нужно изменить высоты частиц таким образом, чтобы добиться рельефа.

Создание рельефа

Взглянем еще раз на добавление частиц:


var geometry = new THREE.Geometry();
var vertex;
for (var i = 0; i < terrainSize; i++) {
    for (var j = 0; j < terrainSize; j++) {
        vertex = new THREE.Vector3();
        vertex.x = (i - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
        vertex.y = 125;
        vertex.z = (j - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
        geometry.vertices.push(vertex);
    }
}

Размер поверхности terrainSize пока фиксированный и равен 200 точкам. Дальше я сдвигаю все точки на половину размера поверхности, чтобы она отображалась по центру сцены, увелчиваю координаты пропорционально и добавляю небольшой случайный сдвиг, чтобы точки не располагались ровными рядками. Координата Y у всех точек одинакова, поэтому поверхность получилась плоской. Наша задача — таким образом изменить координаты Y, чтобы получился рельеф.

Для этого я буду использовать карту высот — это черно-белое изображение, каждая точка которого обозначает высоту рельефа. Чем темнее точка на карте — тем выше точка на поверхности. Такое изображение можно нарисовать в любом графическом редакторе или использовать готовое. Изображение, которое я использовала в примере, можно скачать здесь .

Когда у нас есть изображение, нужно его преобразовать в массив высот. Для этого можно использовать любой язык программирования, который может считать изображение и получить цвет в каждой точке. Получившийся результат я сохранила в js-файл terrain-bitmap.js и подключила его в index.html. Содержимое этого файла будет примерно таким:


var terrain = [
    [74, 74, 75, 76, ...],
    [75, 76, 76, 77, ...],
    ...
];

Для простоты будем считать, что изображение квадратное. В метод init добавим еще один параметр — массив высот:


Particles.init($('#particles'), terrain);

Значение terrainSize сделаем равным размеру массива:


var terrainSize = terrain.length;

Изменим способ генерации частиц:


var vertex, value;
for (var i = 0; i < terrainSize; i++) {
    for (var j = 0; j < terrainSize; j++) {
        value = terrain[i][j];
                    
        vertex = new THREE.Vector3();
        vertex.x = (i - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
        vertex.y = value / 3;
        vertex.z = (j - terrainSize / 2) * 8 + (Math.random() - 0.5) * 8;
        geometry.vertices.push(vertex);
    }
}

Координату Y зададим на основе значения высоты. Я пропорционально уменьшила все высоты, чтобы сделать поверхность менее резкой.

Вот что мы получили в результате:

Мы получили рельефную поверхность из точек, но края этой поверхности слишком резкие. Я добавила вероятность отображения каждой точки в зависимости от расстояния до центра поверхности (чем больше расстояние, тем меньше вероятность) и от значения высоты (чем больше высота, тем больше вероятность):


valueProb = value / terrainSize;
distanceProbX = Math.abs(i1 / (terrainSize / 2));
distanceProbY = Math.abs(j1 / (terrainSize / 2));

if (Math.random() < valueProb
    && Math.random() > distanceProbX
    && Math.random() > distanceProbY) {
    // добавляем точку
}

Результат:

Добавим интерактивности

Чтобы немного оживить нашу поверхность, добавим реакцию на движение мыши. Для этого добавим обработчик события mousemove:


document.addEventListener('mousemove', Particles.onDocumentMouseMove, false);

В методе onDocumentMouseMove запомним смещение курсора мыши:


onDocumentMouseMove: function(event) {
    mouseX = event.clientX - windowHalfX;
}

А в методе render зададим небольшое смещение камеры в зависимости от смещения курсора:


camera.position.x += (mouseX - camera.position.x) * 0.005;
camera.lookAt(scene.position);
renderer.render(scene, camera);

Теперь поверхность будет реагировать на движение курсора мыши и немного поворачиваться.

Заключение

В статье я рассмотрела способ генерации поверхности из частиц. Демо описанного примера можно посмотреть здесь . Это относительно простой пример использования частиц Three.js, на самом деле они позволяют делать удивительные вещи. Если у вас есть вопросы или предложения, как можно улучшить мое решение — дайте мне знать.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться