three.js реализует эффекты взрыва частиц изображения

WebGL three.js
three.js реализует эффекты взрыва частиц изображения

предисловие

Всем привет, это волшебник CSS - alphardex.

Ниже приведен окончательный рендер реализации

explode.gif

Са, маршрут Хадзима!

Готов к работе

авторышаблон three.js: Нажмите вилку в правом нижнем углу, чтобы скопировать копию.

мировая синхронизация

в моемПредыдущий пост в блоге,я говорил о том как синхронизировать мир HTML и мир webgl.В этой статье тоже та же идея.Сначала синхронизировать два мира,а потом уже создавать спецэффекты.

Первая сборка HTML и JS

<div class="relative w-screen h-screen">
  <div class="absolute w-screen h-screen flex-center opacity-0">
    <img src="https://i.loli.net/2021/03/08/uYcvELkr4dqFj9w.jpg" class="w-60 cursor-pointer" alt="" crossorigin="anonymous" />
  </div>
  <div class="particle-explode w-full h-full bg-black"></div>
</div>
class ParticleExplode extends Base {
  // 初始化
  async init() {
    this.createScene();
    this.createPerspectiveCamera();
    this.createRenderer();
    this.createParticleExplodeMaterial();
    await preloadImages();
    this.createPoints();
    this.createClickEffect();
    this.createLight();
    this.trackMousePos();
    this.createOrbitControls();
    this.addListeners();
    this.setLoop();
  }
}

const start = () => {
  const particleExplode = new ParticleExplode(".particle-explode", true);
  particleExplode.init();
};

start();

Синхронизация объектов

class ParticleExplode extends Base {
  constructor(sel: string, debug: boolean) {
    ...
    this.cameraPosition = new THREE.Vector3(0, 0, 1500);
    const fov = this.getScreenFov();
    this.perspectiveCameraParams = {
      fov,
      near: 0.1,
      far: 5000
    };
  }
  // 获取跟屏幕同像素的fov角度
  getScreenFov() {
    return ky.rad2deg(
      2 * Math.atan(window.innerHeight / 2 / this.cameraPosition.z)
    );
  }
}

Предварительная загрузка изображений

import imagesLoaded from "https://cdn.skypack.dev/imagesloaded@4.1.4";

const preloadImages = (sel = "img") => {
  return new Promise((resolve) => {
    imagesLoaded(sel, { background: true }, resolve);
  });
};

синхронизация данных

Создайте DOMMeshObject для синхронизации данных в мирах HTML и webgl.

Здесь следует отметить одну вещь: поскольку в этой статье создаются эффекты частиц, нет необходимостиMesh, с использованиемPoints, Так и будетGeometryПредставлен в виде точечной матрицы

class DOMMeshObject {
  el!: Element;
  rect!: DOMRect;
  mesh!: THREE.Mesh | THREE.Points;
  constructor(
    el: Element,
    scene: THREE.Scene,
    material: THREE.Material = new THREE.MeshBasicMaterial({ color: 0xff0000 }),
    isPoints = false
  ) {
    this.el = el;
    const rect = el.getBoundingClientRect();
    this.rect = rect;
    const { width, height } = rect;
    const geometry = new THREE.PlaneBufferGeometry(
      width,
      height,
      width,
      height
    );
    const mesh = isPoints
      ? new THREE.Points(geometry, material)
      : new THREE.Mesh(geometry, material);
    scene.add(mesh);
    this.mesh = mesh;
  }
  setPosition() {
    const { mesh, rect } = this;
    const { top, left, width, height } = rect;
    const x = left + width / 2 - window.innerWidth / 2;
    const y = -(top + height / 2 - window.innerHeight / 2) + window.scrollY;
    mesh.position.set(x, y, 0);
  }
}

class ParticleExplode extends Base {
  // 创建材质
  createParticleExplodeMaterial() {
    const particleExplodeMaterial = new THREE.ShaderMaterial({
      vertexShader: particleExplodeVertexShader,
      fragmentShader: particleExplodeFragmentShader,
      side: THREE.DoubleSide,
      uniforms: {
        uTime: {
          value: 0
        },
        uMouse: {
          value: new THREE.Vector2(0, 0)
        },
        uResolution: {
          value: new THREE.Vector2(window.innerWidth, window.innerHeight)
        },
        uProgress: {
          value: 0
        },
        uTexture: {
          value: null
        }
      }
    });
    this.particleExplodeMaterial = particleExplodeMaterial;
  }
  // 创建点
  createPoints() {
    const image = document.querySelector("img")!;
    this.image = image;
    const texture = new THREE.Texture(image);
    texture.needsUpdate = true;
    const material = this.particleExplodeMaterial.clone();
    material.uniforms.uTexture.value = texture;
    const imageDOMMeshObj = new DOMMeshObject(
      image,
      this.scene,
      material,
      true
    );
    imageDOMMeshObj.setPosition();
    this.imageDOMMeshObj = imageDOMMeshObj;
  }
}

После создания точечной матрицы экран все равно черный, почему? Поскольку мы забыли установить размер точки в вершинном шейдере, вparticleExplodeVertexShaderдобавить эту строку

gl_PointSize=2.;

1

Видно, что наконец-то на экране появилось изображение, на самом деле это не плоскость, а «плоскость», составленная из тысяч точек.

Теперь вы можете вставить свое любимое изображение, фрагментный шейдерparticleExplodeFragmentShaderкод показывает, как показано ниже

uniform sampler2D uTexture;

varying vec2 vUv;

void main(){
    vec4 color=texture2D(uTexture,vUv);
    if(color.r<.1&&color.g<.1&&color.b<.1){
        discard;
    }
    gl_FragColor=color;
}

2

эффект взрыва

Затем наступил волнующий момент - реализация эффекта взрыва!

шум

Короче говоря, взрыв — это зрелище, образованное большим количеством частиц, беспорядочно движущихся в определенном пространстве. Когда дело доходит до «неравномерности», мы можем сначала подумать о слове - «шум».

Существует множество видов шума, наиболее распространенными являютсяperlin noise,simplex noiseдр., в данной работе используетсяsimplex noiseизcurl noise, поищите в гуглеcurl noise glsl, легко преобразовать следующеешумовой кодПолучите это (Google: это все еще зависит от труда и капитала в критический момент)

vec4 permute(vec4 x){return mod(((x*34.)+1.)*x,289.);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-.85373472095314*r;}

float snoise(vec3 v){
    const vec2 C=vec2(1./6.,1./3.);
    const vec4 D=vec4(0.,.5,1.,2.);
    
    // First corner
    vec3 i=floor(v+dot(v,C.yyy));
    vec3 x0=v-i+dot(i,C.xxx);
    
    // Other corners
    vec3 g=step(x0.yzx,x0.xyz);
    vec3 l=1.-g;
    vec3 i1=min(g.xyz,l.zxy);
    vec3 i2=max(g.xyz,l.zxy);
    
    //  x0 = x0 - 0. + 0.0 * C
    vec3 x1=x0-i1+1.*C.xxx;
    vec3 x2=x0-i2+2.*C.xxx;
    vec3 x3=x0-1.+3.*C.xxx;
    
    // Permutations
    i=mod(i,289.);
    vec4 p=permute(permute(permute(
                i.z+vec4(0.,i1.z,i2.z,1.))
                +i.y+vec4(0.,i1.y,i2.y,1.))
                +i.x+vec4(0.,i1.x,i2.x,1.));
                
                // Gradients
                // ( N*N points uniformly over a square, mapped onto an octahedron.)
                float n_=1./7.;// N=7
                vec3 ns=n_*D.wyz-D.xzx;
                
                vec4 j=p-49.*floor(p*ns.z*ns.z);//  mod(p,N*N)
                
                vec4 x_=floor(j*ns.z);
                vec4 y_=floor(j-7.*x_);// mod(j,N)
                
                vec4 x=x_*ns.x+ns.yyyy;
                vec4 y=y_*ns.x+ns.yyyy;
                vec4 h=1.-abs(x)-abs(y);
                
                vec4 b0=vec4(x.xy,y.xy);
                vec4 b1=vec4(x.zw,y.zw);
                
                vec4 s0=floor(b0)*2.+1.;
                vec4 s1=floor(b1)*2.+1.;
                vec4 sh=-step(h,vec4(0.));
                
                vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;
                vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
                
                vec3 p0=vec3(a0.xy,h.x);
                vec3 p1=vec3(a0.zw,h.y);
                vec3 p2=vec3(a1.xy,h.z);
                vec3 p3=vec3(a1.zw,h.w);
                
                //Normalise gradients
                vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
                p0*=norm.x;
                p1*=norm.y;
                p2*=norm.z;
                p3*=norm.w;
                
                // Mix final noise value
                vec4 m=max(.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.);
                m=m*m;
                return 42.*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),
                dot(p2,x2),dot(p3,x3)));
            }
            
            vec3 snoiseVec3(vec3 x){
                return vec3(snoise(vec3(x)*2.-1.),
                snoise(vec3(x.y-19.1,x.z+33.4,x.x+47.2))*2.-1.,
                snoise(vec3(x.z+74.2,x.x-124.5,x.y+99.4)*2.-1.)
            );
        }
        
        vec3 curlNoise(vec3 p){
            const float e=.1;
            vec3 dx=vec3(e,0.,0.);
            vec3 dy=vec3(0.,e,0.);
            vec3 dz=vec3(0.,0.,e);
            
            vec3 p_x0=snoiseVec3(p-dx);
            vec3 p_x1=snoiseVec3(p+dx);
            vec3 p_y0=snoiseVec3(p-dy);
            vec3 p_y1=snoiseVec3(p+dy);
            vec3 p_z0=snoiseVec3(p-dz);
            vec3 p_z1=snoiseVec3(p+dz);
            
            float x=p_y1.z-p_y0.z-p_z1.y+p_z0.y;
            float y=p_z1.x-p_z0.x-p_x1.z+p_x0.z;
            float z=p_x1.y-p_x0.y-p_y1.x+p_y0.x;
            
            const float divisor=1./(2.*e);
            return normalize(vec3(x,y,z)*divisor);
        }

прикладной шум

После того, как шумовая функция получена, ее можно сразу применить к нашему фрагментному шейдеру.Идея тоже очень проста и сырая: передать информацию о положении в шум, а затем добавить шум в исходное положение.Большое, нужно отлаживать значение медленно, и здесь требуется относительно больше времени

uniform float uTime;
uniform float uProgress;
varying vec2 vUv;

void main(){
    vec3 noise=curlNoise(vec3(position.x*.02,position.y*.008,uTime*.05));
    vec3 distortion=vec3(position.x*2.,position.y,1.)*noise*uProgress;
    vec3 newPos=position+distortion;
    vec4 modelPosition=modelMatrix*vec4(newPos,1.);
    vec4 viewPosition=viewMatrix*modelPosition;
    vec4 projectedPosition=projectionMatrix*viewPosition;
    gl_Position=projectedPosition;
    gl_PointSize=2.;
    
    vUv=uv;
}

пошевеливайся

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

import gsap from "https://cdn.skypack.dev/gsap@3.6.0";

class ParticleExplode extends Base {
  // 创建点击效果
  createClickEffect() {
    const material = this.imageDOMMeshObj.mesh.material as any;
    const image = this.image;
    image.addEventListener("click", () => {
      if (!this.isOpen) {
        gsap.to(material.uniforms.uProgress, {
          value: 3,
          duration: 1
        });
        this.isOpen = true;
      } else {
        gsap.to(material.uniforms.uProgress, {
          value: 0,
          duration: 1
        });
        this.isOpen = false;
      }
    });
  }
  // 动画
  update() {
    const elapsedTime = this.clock.getElapsedTime();
    if (this.imageDOMMeshObj) {
      const material = this.imageDOMMeshObj.mesh.material as any;
      material.uniforms.uTime.value = elapsedTime;
    }
  }
}

explode.gif

адрес проекта

Particle Explode