Давайте ослеплять вместе - 3D анимация частиц

JavaScript визуализация данных Canvas three.js
Давайте ослеплять вместе - 3D анимация частиц

Об авторе Coco Ant Financial Data Experience Технологическая команда

Демонстрация эффекта

Эффект анимации 3D-частиц выглядит следующим образом:
embed: sky.mov

Конфигурация настоящего примера несложная, в основном разделена на следующие разделы:

  1. градиентный фон
  2. Постоянное переключение 3D-моделей частиц (рассеянные точки, попадающие в поле, также являются формой моделей частиц)
  3. Текст для одновременного переключения

Реализация в основном основана на threejs.Далее я объясню реализацию каждой части отдельно, но не буду знакомить с основами. Основной контент можно найти на официальном сайте~

градиентный фон

Фон сцены может принимать Color, Texture или CubeTexture. В этом примере мы используем Texture для достижения эффекта градиента. Текстура может принимать холст или изображения. Здесь мы используем холст для рисования эффектов градиента. Конкретный код выглядит следующим образом:

const canvas = document.createElement('canvas');
canvas.width = window.innerWidth; 
canvas.height = window.innerHeight;
const context = canvas.getContext('2d');
const gradient = context.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, '#4e22b7');
gradient.addColorStop(1, '#3292ff');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.needsUpdate = true;

const scene = new THREE.Scene();
scene.background = texture;

Уменьшите количество частиц модели

Далее я познакомлю вас с черной технологией — уменьшением количества модельных частиц. Обычно дизайнер выдает вам ту модель, которая соответствует требованиям в дизайн-проекте, и количество частиц не будет больше, чем вам нужно, но бывают и исключения. Кроме того, количество частиц является одним из ключевых факторов, влияющих на производительность.В этом примере я протестировал частицы мощностью 1 Вт, чтобы анимация застряла (конечно, это во многом связано с производительностью бега). Если у вас возникли проблемы в этой области, то этот метод может быть вам полезен.

Идея реализации: Вычислить расстояние между соседними точками по осям X, Y и Z соответственно.Если расстояние больше расстояния и меньше заданного значения, удалить его. Конкретный код выглядит следующим образом:

// pos为粒子的数据,单项数据分别含x,y,z坐标值 {x, y, z}
const all = [];
const yObjs = {};
const xObjs = {};
const zObjs = {};

// 对所有点按照x,y,z值进行分组,分组后的数据分别存在xObjs,yObjs,zObjs
for (var i = pos.length-1; i>=0; i--) {
    const p = pos[i];
    const yKey = getKey(p.y);
    const xKey = getKey(p.x);
    const zKey = getKey(p.z);
    
    if (!yObjs[yKey]) { yObjs[yKey] = []; }
    if (!xObjs[xKey]) { xObjs[xKey] = []; }
    if (!zObjs[zKey]) { zObjs[zKey] = []; }

    const item = {x, y, z, r, g, b};
    yObjs[yKey].push(item);
    xObjs[xKey].push(item);
    zObjs[zKey].push(item);
    all.push(item);
}

// 对x,y,z值进行排序
const xKeys = orderAscKeys(xObjs);
const yKeys = orderAscKeys(yObjs);
const zKeys = orderAscKeys(zObjs);

// 提取在x,y,z轴上需删除的坐标值
const xDels = getDelKeys(xKeys, 4);  // 4为点与点的间距
const yDels = getDelKeys(yKeys, 4);
const zDels = getDelKeys(zKeys, 4);

const result = [];

for (var i = all.length-1; i>=0; i--) {
    const item = all[i];
    if ( !(xDels.indexOf(getKey(item.x))>-1 || yDels.indexOf(getKey(item.y))>-1 || zDels.indexOf(getKey(item.z))>-1 )) {
        result.push(item);
    }
}

return result;

function getKey(x) {
    let r = x.toFixed(1);
    if (r==='-0.0') {
        r = '0.0'
    }
    return r;
}  

function orderAscKeys (obj) {
    return Object.keys(obj).sort((a, b)=>{
        return Number(a) - Number(b);
    });
}
function getDelKeys(keys, d) {
    const dels = [];
    keys.reduce((prev, curr, idx)=>{
        let res = curr;
        if (curr-prev < d) {
            dels.push(curr);
            res = prev;
        }
        return res;
    });
    return dels;
}

Примечание: этот метод предназначен только для моделей с четким упорядочением частиц.Если упорядочение частиц самой модели неупорядочено, эффект этого метода может быть неидеальным.

Анимация переключения моделей

Создание объектов частиц

Во всем анимационном эффекте частицы плавно объединяют разные модели, меняя свое положение, как это сделать?
Если вы даете каждой модели новую геометрию, на этот раз вы торопитесь, и эффект не идеален. Лучший способ справиться с этим — использовать геометрию для этих 5 моделей и преобразовать модель, изменив свойство вершин. Однако следует отметить, что свойство vertices может изменять значение каждого элемента в нем, но не может добавлять количество элементов после определения. Это требует, чтобы количество значений вершин в исходном определении геометрии было таким, которое имеет наибольшее количество точек в нескольких моделях.

const config = this.config;
const textureLoader = new THREE.TextureLoader();
textureLoader.crossOrigin = '';
const mapDot = textureLoader.load('img/gradient.png');  // 圆点
this.mapDot = mapDot;

const geometry = new THREE.Geometry();

const count = config.totalCount;  // 四个模型中最多个粒子总数值
for (let i = 0; i < count; i++) {
    let x = 0, y = 0, z = 0;
    x = Math.random() * 2000 - 1000;
    y = Math.random() * 2000 - 1000;
    z = Math.random() * 8000 - 4000;

    geometry.vertices.push(new THREE.Vector3(x, y, z));
    geometry.colors.push(new THREE.Color(1, 1, 1));
}

const material = new THREE.PointsMaterial({
    size: 20,
    map: mapDot,
    depthTest: true,
    alphaTest: .1,
    opacity: 1,
    side: THREE.DoubleSide,
    transparent: !0,
    vertexColors: THREE.VertexColors,
});
const points = new THREE.Points(geometry, material);

// 调整模型姿势
points.rotation.z = Math.PI;
points.rotation.y = Math.PI;
points.rotation.x = -Math.PI * .3;

points.position.y = 240;
points.position.x = 100;
points.position.z = 240;
this.scene.add(points);

this.points = points;

WebGL использует правостороннюю систему координат, в то время как двухмерная система координат холста расширяет ось x вправо, а ось y вниз, поскольку существует такая разница, трехмерная модель, созданная непосредственно из изображения, будет быть инвертированным. При фактической обработке мы можем настроить модель, отсортировав модель, или преобразовать значение координаты каждой точки. Конкретный метод может быть выбран в соответствии с реальным желаемым эффектом.

Разница в системе координат, как показано на рисунке:

坐标系对比

переключатель состояния

Весь процесс анимации имеет следующие состояния:

  1. Входная анимация прелюдии (т. е. состояние разброса) для подготовки данных и эффектов перехода анимации
  2. Когда идет анимация переключения моделей
  3. Интервал переключения моделей в состоянии покоя

Конкретный код для управления состоянием и переключения выглядит следующим образом:

// 一切模型都已经准备妥当
if (this.objectsData && this.points && this.index !== undefined) {
    const config = this.config;
    const len = config.objs.length;
    const idx = this.index % len;  // 当前模型序号
    const item = this.objectsData[idx];
    const geometry = this.points.geometry;

    this.points.material.needsUpdate = true;
    geometry.verticesNeedUpdate = true;
    geometry.colorsNeedUpdate = true;

    // 前奏因数据准备时间不较长,不额外加延迟时间
    const delay = (this.index === 0) ? config.delay * 60 * 0 : config.delay * 60;

    if (item.waiting < delay) {
        // 等待时间,包含了前奏(1)和切换间隔时间(3)
        item.waiting++;
    } else if (item.waiting >= delay && item.count < config.totalCount - 1) {
        // 动画进行时(2)
        let nd;
        if (item.count === 0) {
            config.onLeave && typeof config.onLeave === 'function' && config.onLeave();

            nd = idx;

            const prevIdx = (idx - 1 > -1) ? idx - 1 : len - 1;
            const prev = this.objectsData[prevIdx];

            if (prev) {
                // 执行下一个模型切换动画时,将前一个的执行时计数和休息时计数都归零
                prev.count = 0;
                prev.waiting = 0;
            }
        }
        // 执行动画
        this.particleAnimation(this.points, item, nd);

    } else if (item.waiting >= delay && item.count >= config.totalCount - 1) {
        // 切换到下一个模型
        this.index++;
    }

}

выполнить анимацию

Анимации переключения моделей выполняются пакетно, не все частицы начинают двигаться вместе.
Преимущества этого:

  1. Избегайте зависания экрана из-за чрезмерных вычислений в пределах единицы времени (16,67 мс); единица времени определяется частотой выполнения requestAnimationFrame.
  2. Группировка анимаций сделает анимацию более многослойной.
/**
 * points: 粒子对象
 * item: 当前模型数据
 * idx: 当前模型序号
 */
particleAnimation(points, item, idx) {
    const geometry = points.geometry;
    const vertices = geometry.vertices;
    const colors = geometry.colors;
    const len = vertices.length;

    if (item.count >= len) { return; }

    if (!item.vTween) {
        item.vTween = [];
    }

    const config = this.config;
    
    let isOdd = this.index % 2===0;
    // 每组动画执行的粒子个数
    const cnt = 1000;
    for (let j = item.count, l = item.count + cnt; j < l && j < len - 1; j++) {
        const n = j % item.length;
        const p = item.data[n];
        if (!p) { return; }

        // TWEEN只new一次,减少多次实例化的开销
        if (!item.vTween[j] && vertices[j]) {
            item.vTween.push(new TWEEN.Tween(vertices[j])
                .easing(TWEEN.Easing.Exponential.In)
            );
        }

        item.vTween[j].stop();
        
        // 奇偶序号的模型位置不一样,调整x坐标数据
        const x = (isOdd) ? p.x : p.x-400;
        item.vTween[j].to({ x, y: p.y, z: p.z }, config.speed).start();
    }

    item.count = item.count + cnt;
}

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

заключительные замечания

Анимация текстовой части реализована с помощью css3.В Интернете есть много документов, связанных с анимацией css3, которые не будут подробно описываться.Здесь соответствующие коды стилей просто перечислены для справки.

.leaving {
    transition: transform .7s, opacity .7s;
    transform: translate3d(0px, -205%, 0);
    opacity: 0;
}
.entering {
    transition: opacity .7s;
    opacity: 1;
}

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

Заинтересованные студенты могут подписаться на эту колонку или отправить свои резюме по адресу 'shanshan.hongss####alibaba-inc.com'.replace('####', '@'), добро пожаловать, присоединяйтесь~~

Оригинальный адрес:GitHub.com/proto team/no…