three.js创造时空裂缝特效实现示例

JavaScript/前端
354
0
0
2023-06-21
目录
  • 效果图
  • 建模
  • 多边形形状
  • 随机多边形
  • 漂浮动画
  • 光照
  • 后期处理

效果图


最近受到轮回系作品《寒蝉鸣泣之时》中时空裂缝场景的启发,我用three.js实现了一个实时渲染的时空裂缝场景。本文将简要地介绍下实现该效果的要点。

以下特效全屏观看效果最佳~

<div id="sketch"></div>
<div><div class="fixed z- top-0 left-0 loader-screen w-screen h-screen transition-all duration-300 bg-black"><div class="absolute hv-center"><div class="loading text-white text-xl tracking-widest whitespace-no-wrap"><span style="--i:">L</span><span style="--i:">O</span><span style="--i:">A</span><span style="--i:">D</span><span style="--i:">I</span><span style="--i:">N</span><span style="--i:">G</span></div></div></div><div class="fixed z- top-0 left-0 w-screen h-screen text-white text-xl"><div class="absolute hv-center"><div class="scene- space-y-10 text-center whitespace-no-wrap"><div class="space-y-"><div class="shuffle-text shuffle-text-">欢迎来到时空裂缝!</div><div class="shuffle-text shuffle-text-">在这里,你可以体验穿梭时空的感觉!</div><div class="shuffle-text shuffle-text-">准备好了,就点击下面的按钮吧~</div></div><button data-text="开始穿梭"class="dash-btn btn btn-primary btn-ghost btn-border-stroke  btn-text-float-up"><div class="btn-borders"><div class="border-top"></div><div class="border-right"></div><div class="border-bottom"></div><div class="border-left"></div></div><span class="btn-text">开始穿梭</span></button></div><div class="scene- space-y-10 text-center whitespace-no-wrap"><div class="space-y-"><div class="shuffle-text shuffle-text-">穿梭的感觉如何?</div><div class="shuffle-text shuffle-text-">如果觉得不错,可以推荐给其他小伙伴~</div><div class="shuffle-text shuffle-text-">我是alphardex,一个爱写特效的前端</div></div></div></div></div>
</div>

CSS 

body {
    margin:;
    overflow: hidden;
}
#sketch {
    width:vw;
    height:vh;
    background: black;
}
body {
    background: black;
}
* {
    user-select: none;
}
#sketch {
    opacity:;
}
.scene-,
.scene- {
    display: none;
}
.loading span {
    animation: blur.5s calc(var(--i) / 5 * 1s) alternate infinite;
}
@keyframes blur {
    to {
        filter: blur(px);
    }
}
.shuffle-text {
    display: none;
    opacity:.6;
}
.dash-btn {
    opacity:;
    pointer-events: none;
}
.btn {
    --hue:;
    --ease-in-duration:.25s;
    --ease-in-exponential: cubic-bezier(.95, 0.05, 0.795, 0.035);
    --ease-out-duration:.65s;
    --ease-out-delay: var(--ease-in-duration);
    --ease-out-exponential: cubic-bezier(.19, 1, 0.22, 1);
    position: relative;
    padding:rem 3rem;
    font-size:rem;
    line-height:.5;
    color: white;
    text-decoration: none;
    background-color: hsl(var(--hue),%, 41%);
    border:px solid hsl(var(--hue), 100%, 41%);
    outline: transparent;
    overflow: hidden;
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    transition:.25s;
}
.btn:hover {
    background: hsl(var(--hue),%, 31%);
}
.btn-primary {
    --hue:;
}
.btn-ghost {
    color: hsl(var(--hue),%, 41%);
    background-color: transparent;
    border-color: hsl(var(--hue),%, 41%);
}
.btn-ghost:hover {
    color: white;
}
.btn-border-stroke {
    border-color: hsla(var(--hue),%, 41%, 0.35);
}
.btn-border-stroke .btn-borders {
    position: absolute;
    top:;
    left:;
    width:%;
    height:%;
}
.btn-border-stroke .btn-borders .border-top {
    position: absolute;
    top:;
    width:%;
    height:px;
    background: hsl(var(--hue),%, 41%);
    transform: scaleX();
    transform-origin: left;
}
.btn-border-stroke .btn-borders .border-right {
    position: absolute;
    right:;
    width:px;
    height:%;
    background: hsl(var(--hue),%, 41%);
    transform: scaleY();
    transform-origin: bottom;
}
.btn-border-stroke .btn-borders .border-bottom {
    position: absolute;
    bottom:;
    width:%;
    height:px;
    background: hsl(var(--hue),%, 41%);
    transform: scaleX();
    transform-origin: left;
}
.btn-border-stroke .btn-borders .border-left {
    position: absolute;
    left:;
    width:px;
    height:%;
    background: hsl(var(--hue),%, 41%);
    transform: scaleY();
    transform-origin: bottom;
}
.btn-border-stroke .btn-borders .border-left {
    transition: var(--ease-out-duration) var(--ease-out-delay) var(--ease-out-exponential);
}
.btn-border-stroke .btn-borders .border-bottom {
    transition: var(--ease-out-duration) var(--ease-out-delay) var(--ease-out-exponential);
}
.btn-border-stroke .btn-borders .border-right {
    transition: var(--ease-in-duration) var(--ease-in-exponential);
}
.btn-border-stroke .btn-borders .border-top {
    transition: var(--ease-in-duration) var(--ease-in-exponential);
}
.btn-border-stroke:hover {
    color: hsl(var(--hue),%, 41%);
    background: transparent;
}
.btn-border-stroke:hover .border-top,
.btn-border-stroke:hover .border-bottom {
    transform: scaleX();
}
.btn-border-stroke:hover .border-left,
.btn-border-stroke:hover .border-right {
    transform: scaleY();
}
.btn-border-stroke:hover .border-left {
    transition: var(--ease-in-duration) var(--ease-in-exponential);
}
.btn-border-stroke:hover .border-bottom {
    transition: var(--ease-in-duration) var(--ease-in-exponential);
}
.btn-border-stroke:hover .border-right {
    transition: var(--ease-out-duration) var(--ease-out-delay) var(--ease-out-exponential);
}
.btn-border-stroke:hover .border-top {
    transition: var(--ease-out-duration) var(--ease-out-delay) var(--ease-out-exponential);
}
.btn-text-float-up::after {
    position: absolute;
    content: attr(data-text);
    top:;
    left:;
    width:%;
    height:%;
    display: flex;
    justify-content: center;
    align-items: center;
    opacity:;
    transform: translateY(%);
    transition:.25s ease-out;
}
.btn-text-float-up .btn-text {
    display: block;
    transition:.75s 0.1s var(--ease-out-exponential);
}
.btn-text-float-up:hover .btn-text {
    opacity:;
    transform: translateY(-%);
    transition:.25s ease-out;
}
.btn-text-float-up:hover::after {
    opacity:;
    transform: translateY();
    transition:.75s 0.1s var(--ease-out-exponential);
}

JS

const vertexShader = `
uniform float iTime;
uniform vec iResolution;
uniform vec iMouse;
varying vec vUv;
varying vec vNormal;
varying vec vMvPosition;
varying vec vPosition;
uniform vec uMouse;
uniform float uRandom;
uniform float uLayerId;
// transform
mat rotation2d(float angle){
    float s=sin(angle);
    float c=cos(angle);
    return mat(
        c,-s,
        s,c
    );
}
mat rotation3d(vec3 axis,float angle){
    axis=normalize(axis);
    float s=sin(angle);
    float c=cos(angle);
    float oc=.-c;
    return mat(
        oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,.,
        oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,.,
        oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,.,
.,0.,0.,1.
    );
}
vec rotate(vec2 v,float angle){
    return rotationd(angle)*v;
}
vec rotate(vec3 v,vec3 axis,float angle){
    return(rotationd(axis,angle)*vec4(v,1.)).xyz;
}
vec distort(vec3 p){
    vec tx1=vec3(-uMouse.x*uRandom*.05,-uMouse.y*uRandom*.02,0.);
    p+=tx;
    float angle=iTime*uRandom;
    p=rotate(p,vec(0.,1.,0.),angle);
    vec tx2=vec3(-uMouse.x*uRandom*.5,-uMouse.y*uRandom*.2,0.);
    p+=tx;
    p*=(.-uLayerId*.5);
    return p;
}
void main(){
    vec p=position;
    vec N=normal;
    p=distort(p);
    N=distort(N);
    gl_Position=projectionMatrix*modelViewMatrix*vec(p,1.);
    vUv=uv;
    vNormal=N;
    vMvPosition=modelViewMatrix*vec(p,1.);
    vPosition=p;
}
`;
const fragmentShader = `
uniform float iTime;
uniform vec iResolution;
uniform vec iMouse;
varying vec vUv;
varying vec vNormal;
varying vec vMvPosition;
varying vec vPosition;
uniform samplerD uTexture;
uniform vec uLightPosition;
uniform vec uLightColor;
uniform float uRandom;
uniform vec uMouse;
// transform
mat rotation2d(float angle){
    float s=sin(angle);
    float c=cos(angle);
    return mat(
        c,-s,
        s,c
    );
}
mat rotation3d(vec3 axis,float angle){
    axis=normalize(axis);
    float s=sin(angle);
    float c=cos(angle);
    float oc=.-c;
    return mat(
        oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,.,
        oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,.,
        oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,.,
.,0.,0.,1.
    );
}
vec rotate(vec2 v,float angle){
    return rotationd(angle)*v;
}
vec rotate(vec3 v,vec3 axis,float angle){
    return(rotationd(axis,angle)*vec4(v,1.)).xyz;
}
// lighting
float saturate(float a){
    return clamp(a,.,1.);
}
float diffuse(vec n,vec3 l){
    float diff=saturate(dot(n,l));
    return diff;
}
float specular(vec n,vec3 l,float shininess){
    float spec=pow(saturate(dot(n,l)),shininess);
    return spec;
}
float blendSoftLight(float base,float blend){
    return(blend<.)?(2.*base*blend+base*base*(1.-2.*blend)):(sqrt(base)*(2.*blend-1.)+2.*base*(1.-blend));
}
vec blendSoftLight(vec3 base,vec3 blend){
    return vec(blendSoftLight(base.r,blend.r),blendSoftLight(base.g,blend.g),blendSoftLight(base.b,blend.b));
}
// distort
vec distort(vec2 p){
    vec m=uMouse;
    p.x-=(uRandom-m.x*.)*.5;
    p.y-=uRandom*.-iTime*.1;
    p.x-=.;
    p.y-=.;
    p=rotate(p,uRandom);
    p*=.;
    return p;
}
vec distortNormal(vec3 p){
    p*=vec(-1.*uRandom*15.,-1.*uRandom*15.,30.5);
    return p;
}
// lighting
vec lighting(vec3 tex,vec3 normal){
    vec viewLightPosition=viewMatrix*vec4(uLightPosition,0.);
    vec N=normalize(normal);
    vec L=normalize(viewLightPosition.xyz);
    vec dif=tex*uLightColor*diffuse(N,L);
    vec C=-normalize(vMvPosition.xyz);
    vec R=reflect(-L,N);
    vec spe=uLightColor*specular(R,C,500.);
    vec lightingColor=vec4(dif+spe,.5);
    vec softlight=blendSoftLight(tex,spe);
    float dotRC=dot(R,C);
    float theta=acos(dotRC/length(R)*length(C));
    float a=.-theta*.3;
    vec col=vec4(tex,a*.01)+vec4(softlight,.02)+(lightingColor*a);
    return col;
}
void main(){
    vec p=vUv;
    vec N=vNormal;
    p=distort(p);
    N=distortNormal(N);
    vec tex=texture2D(uTexture,p);
    vec col=tex;
    col=lighting(tex.xyz,N);
    gl_FragColor=col;
}
`;
const vertexShader = `
uniform float iTime;
uniform vec iResolution;
uniform vec iMouse;
varying vec vUv;
void main(){
    vec p=position;
    gl_Position=projectionMatrix*modelViewMatrix*vec(p,1.);
    vUv=uv;
}
`;
const fragmentShader = `
uniform float iTime;
uniform vec iResolution;
uniform vec iMouse;
uniform samplerD tDiffuse;
varying vec vUv;
uniform float uRGBShift;
vec RGBShift(sampler2D t,vec2 rUv,vec2 gUv,vec2 bUv){
    vec color1=texture2D(t,rUv);
    vec color2=texture2D(t,gUv);
    vec color3=texture2D(t,bUv);
    vec color=vec4(color1.r,color2.g,color3.b,color2.a);
    return color;
}
highp float random(vec co)
{
    highp float a=.9898;
    highp float b=.233;
    highp float c=.5453;
    highp float dt=dot(co.xy,vec(a,b));
    highp float sn=mod(dt,.14);
    return fract(sin(sn)*c);
}
void main(){
    vec p=vUv;
    vec col=vec4(0.);
    // RGB Shift
    float n=random(p+mod(iTime,.))*.1+.5;
    vec offset=vec2(cos(n),sin(n))*.0025*uRGBShift;
    vec rUv=p+offset;
    vec gUv=p;
    vec bUv=p-offset;
    col=RGBShift(tDiffuse,rUv,gUv,bUv);
    gl_FragColor=col;
}
`;
class Fragment extends kokomi.Component {
  constructor(base, config = {}) {
    super(base);
    const { material, points } = config;
    this.points = kokomi.polySort(points);
    // const geometry = new THREE.PlaneGeometry(.1, 0.1, 16, 16);const shape = kokomi.createPolygonShape(this.points, {
      scale:.01,
    });
    const geometry = new THREE.ExtrudeGeometry(shape, {
      steps:,
      depth:.0001,
      bevelEnabled: true,
      bevelThickness:.0005,
      bevelSize:.0005,
      bevelSegments:,
    });
    geometry.center();
    const matClone = material.clone();
    matClone.uniforms.uRandom.value = THREE.MathUtils.randFloat(.1, 1.1);
    const mesh = new THREE.Mesh(geometry, matClone);
    this.mesh = mesh;
    const uj = new kokomi.UniformInjector(this.base);
    this.uj = uj;
  }
  addExisting() {
    this.base.scene.add(this.mesh);
  }
  update() {
    this.uj.injectShadertoyUniforms(this.mesh.material.uniforms);
    gsap.to(this.mesh.material.uniforms.uMouse.value, {
      x: this.base.interactionManager.mouse.x,
    });
    gsap.to(this.mesh.material.uniforms.uMouse.value, {
      y: this.base.interactionManager.mouse.y,
    });
    const lp = this.base.clock.elapsedTime *.01;
    this.mesh.material.uniforms.uLightPosition.value.copy(
      new THREE.Vector(Math.cos(lp), Math.sin(lp), 10)
    );
  }
}
class FragmentGroup extends kokomi.Component {
  constructor(base, config = {}) {
    super(base);
    const { material, layerId =, polygons } = config;
    const g = new THREE.Group();
    this.g = g;
    const frags = polygons.map((points, i) => {
      const frag = new Fragment(this.base, {
        material,
        points,
      });
      frag.addExisting();
      const firstPoint = frag.points[];
      frag.mesh.position.set(
        firstPoint.x *.01,
        firstPoint.y * -.01,
        THREE.MathUtils.randFloat(-, -1)
      );
      frag.mesh.material.uniforms.uLayerId.value = layerId;
      g.add(frag.mesh);
      return frag;
    });
    this.g.position.z = - 1.5 * layerId;
    this.frags = frags;
  }
  addExisting() {
    this.base.scene.add(this.g);
  }
}
const generatePolygons = (config = {}) => {
  const { gridX =, gridY = 20, maxX = 9, maxY = 9 } = config;
  const polygons = [];
  for (let i =; i < gridX; i++) {
    for (let j =; j < gridY; j++) {
      const points = [];
      let edgeCount =;
      const randEdgePossibility = Math.random();
      if (randEdgePossibility > && randEdgePossibility <= 0.2) {
        edgeCount =;
      } else if (randEdgePossibility >.2 && randEdgePossibility <= 0.55) {
        edgeCount =;
      } else if (randEdgePossibility >.55 && randEdgePossibility <= 0.9) {
        edgeCount =;
      } else if (randEdgePossibility >.9 && randEdgePossibility <= 0.95) {
        edgeCount =;
      } else if (randEdgePossibility >.95 && randEdgePossibility <= 1) {
        edgeCount =;
      }
      let firstPoint = {
        x:,
        y:,
      };
      let angle = THREE.MathUtils.randFloat(, 2 * Math.PI);
      for (let k =; k < edgeCount; k++) {
        if (k ===) {
          firstPoint = {
            x: (i % maxX) *,
            y: (j % maxY) *,
          };
          points.push(firstPoint);
        } else {
          // random polarconst r =;
          angle += THREE.MathUtils.randFloat(, Math.PI / 2);
          const anotherPoint = {
            x: firstPoint.x + r * Math.cos(angle),
            y: firstPoint.y + r * Math.sin(angle),
          };
          points.push(anotherPoint);
        }
      }
      polygons.push(points);
    }
  }
  return polygons;
};
class FragmentWorld extends kokomi.Component {
  constructor(base, config = {}) {
    super(base);
    const { material } = config;
    const fgsContainer = new THREE.Group();
    this.base.scene.add(fgsContainer);
    fgsContainer.position.copy(new THREE.Vector(-0.36, 0.36, 0.1));
    // fragment groupsconst polygons = generatePolygons();
    const fgs = [...Array().keys()].map((item, i) => {
      const fg = new FragmentGroup(this.base, {
        material,
        layerId: i,
        polygons,
      });
      fg.addExisting();
      fgsContainer.add(fg.g);
      return fg;
    });
    this.fgs = fgs;
    // clone group for infinite loopconst fgsContainer = new THREE.Group().copy(fgsContainer.clone());
    fgsContainer.position.y = fgsContainer.position.y - 1;
    const totalG = new THREE.Group();
    totalG.add(fgsContainer);
    totalG.add(fgsContainer);
    this.totalG = totalG;
    // animethis.floatDistance =;
    this.floatSpeed =;
    this.floatMaxDistance =;
    this.isDashing = false;
  }
  addExisting() {
    this.base.scene.add(this.totalG);
  }
  update() {
    this.floatDistance += this.floatSpeed;
    const y = this.floatDistance *.001;
    if (y > this.floatMaxDistance) {
      this.floatDistance =;
    }
    if (this.totalG) {
      this.totalG.position.y = y;
    }
  }
  speedUp() {
    gsap.to(this, {
      floatSpeed:,
      duration:,
      ease: "power.in",
    });
  }
  speedDown() {
    gsap.to(this, {
      floatSpeed:,
      duration:,
      ease: "power.inOut",
    });
  }
  async dash(duration =, cb) {
    if (this.isDashing) {
      return;
    }
    this.isDashing = true;
    this.speedUp();
    await kokomi.sleep(duration);
    if (cb) {
      cb();
    }
    this.speedDown();
  }
  changeTexture(name) {
    this.fgs.forEach((fg) => {
      fg.frags.forEach((frag) => {
        const tex = this.base.am.items[name];
        tex.wrapS = THREE.RepeatWrapping;
        tex.wrapT = THREE.RepeatWrapping;
        frag.mesh.material.uniforms.uTexture.value = tex;
      });
    });
  }
}
class Sketch extends kokomi.Base {
  create() {
    this.camera.position.set(, 0, 1.5);
    this.camera.fov =;
    this.camera.near =.01;
    this.camera.far =;
    this.camera.updateProjectionMatrix();
    const resourceList = [
      {
        name: "tex",
        type: "texture",
        path: "https://s.loli.net/2022/11/19/cqOho3ZKCXfTdzw.jpg",
      },
      {
        name: "tex",
        type: "texture",
        path: "https://s.loli.net/2022/11/20/8E6yHP9kAawc7Wr.jpg",
      },
    ];
    const am = new kokomi.AssetManager(this, resourceList);
    this.am = am;
    am.on("ready", async () => {
      const tex = am.items["tex"];
      tex.wrapS = THREE.RepeatWrapping;
      tex.wrapT = THREE.RepeatWrapping;
      const uj = new kokomi.UniformInjector(this);
      const material = new THREE.ShaderMaterial({
        vertexShader,
        fragmentShader,
        side: THREE.DoubleSide,
        transparent: true,
        uniforms: {
          ...uj.shadertoyUniforms,
          uTexture: {
            value: tex,
          },
          uLightPosition: {
            value: new THREE.Vector(-0.2, -0.2, 3),
          },
          uLightColor: {
            value: new THREE.Color("#eeeeee"),
          },
          uRandom: {
            value: THREE.MathUtils.randFloat(.1, 1.1),
          },
          uMouse: {
            value: new THREE.Vector(0.5, 0.5),
          },
          uLayerId: {
            value:,
          },
        },
      });
      // fragment worldconst fw = new FragmentWorld(this, {
        material,
      });
      fw.addExisting();
      this.fw = fw;
      // postprocessingconst ce = new kokomi.CustomEffect(this, {
        vertexShader: vertexShader,
        fragmentShader: fragmentShader,
        uniforms: {
          uRGBShift: {
            value:,
          },
        },
      });
      ce.addExisting();
      // DOMconst shuffleText = (sel) => {
        gsap.set(sel, {
          display: "block",
        });
        const st = new ShuffleText(document.querySelector(sel));
        st.start();
      };
      const start = async () => {
        document.querySelector(".loader-screen").classList.add("hollow");
        await kokomi.sleep();
        gsap.to("#sketch", {
          opacity:,
        });
        await kokomi.sleep();
        await startScene();
      };
      const startScene = async () => {
        gsap.set(".scene-", {
          display: "block",
        });
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        gsap.to(".scene-", {
          opacity:,
          pointerEvents: "none",
        });
      };
      const startScene = async () => {
        gsap.set(".scene-", {
          display: "block",
        });
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        shuffleText(".shuffle-text-");
        await kokomi.sleep();
        gsap.to(".dash-btn", {
          opacity:,
          pointerEvents: "auto",
        });
        document
          .querySelector(".dash-btn")
          .addEventListener("click", async () => {
            gsap.set(".dash-btn", {
              pointerEvents: "none",
            });
            gsap.to(".scene-", {
              opacity:,
              pointerEvents: "none",
              display: "none",
            });
            await this.fw.dash(, () => {
              this.fw.changeTexture("tex");
            });
            await kokomi.sleep();
            await startScene();
          });
      };
      await start();
    });
  }
}
const createSketch = () => {
  const sketch = new Sketch();
  sketch.create();
  return sketch;
};
createSketch();

运行效果 

建模

多边形形状

首先,创造一个最初始的平面

建模,也就是定制化geometry

要想创建玻璃碎片一般的形状的话,也就是要创造一个多边形的形状

这就要用到kokomi.js的这2个函数createPolygonShapepolySort:前者能接收一系列的点来创造一个多边形Shape,后者能给无序的点进行排序以符合多边形的描画

创建形状Shape后,再传进ExtrudeGeometry将其3D化成geometry即可,这里depth等值故意设得很小,是为了模拟玻璃碎片的纤细程度

let points = [
  { x:, y: 0 },
  { x:, y: 0 },
  { x:, y: 45 },
  { x:, y: 25 },
];
points = kokomi.polySort(points);
const shape = kokomi.createPolygonShape(points, {
  scale:.01,
});
const geometry = new THREE.ExtrudeGeometry(shape, {
  steps:,
  depth:.0001,
  bevelEnabled: true,
  bevelThickness:.0005,
  bevelSize:.0005,
  bevelSegments:,
});
geometry.center();

随机多边形

为了创建随机的多边形,我特意设计了一套算法,大致是这样的:

  • 多边形是按二维网格排布的,这样就能尽可能避免有重合的情况出现
  • 多边形的边数edgeCount按个人喜好用随机概率来控制
  • 多边形的第一个点决定了它在网格上的位置,其他的点是以它为圆心延伸出来的随机角度的点(跟圆有关因此用到了极坐标公式)
const generatePolygons = (config = {}) => {
  const { gridX =, gridY = 20, maxX = 9, maxY = 9 } = config;
  const polygons = [];
  for (let i =; i < gridX; i++) {
    for (let j =; j < gridY; j++) {
      const points = [];
      let edgeCount =;
      const randEdgePossibility = Math.random();
      if (randEdgePossibility > && randEdgePossibility <= 0.2) {
        edgeCount =;
      } else if (randEdgePossibility >.2 && randEdgePossibility <= 0.55) {
        edgeCount =;
      } else if (randEdgePossibility >.55 && randEdgePossibility <= 0.9) {
        edgeCount =;
      } else if (randEdgePossibility >.9 && randEdgePossibility <= 0.95) {
        edgeCount =;
      } else if (randEdgePossibility >.95 && randEdgePossibility <= 1) {
        edgeCount =;
      }
      let firstPoint = {
        x:,
        y:,
      };
      let angle = THREE.MathUtils.randFloat(, 2 * Math.PI);
      for (let k =; k < edgeCount; k++) {
        if (k ===) {
          firstPoint = {
            x: (i % maxX) *,
            y: (j % maxY) *,
          };
          points.push(firstPoint);
        } else {
          // random polarconst r =;
          angle += THREE.MathUtils.randFloat(, Math.PI / 2);
          const anotherPoint = {
            x: firstPoint.x + r * Math.cos(angle),
            y: firstPoint.y + r * Math.sin(angle),
          };
          points.push(anotherPoint);
        }
      }
      polygons.push(points);
    }
  }
  return polygons;
};

用该算法来创建多边形组,再调整下相机和多边形组的位置和缩放,就有了下图的效果

漂浮动画

将多边形组整体向上偏移,超出界限则重置高度

let floatDistance =;
let floatSpeed =;
let floatMaxDistance =;
this.update(() => {
  floatDistance += floatSpeed;
  const y = floatDistance *.001;
  if (y > floatMaxDistance) {
    floatDistance =;
  }
  totalG.position.y = y;
});

将相机靠近,你就会觉得像是每个多边形在上升(其实是整体的容器在上升)

接下来还有2点可以优化下:

  • 要想达成一种大小错落的层次感,我们可以拷贝一份多边形组,将其的z轴位置往后移即可
  • 要想达成无限上升的动画“假象”,我们需要再整体拷贝一份多边形组(包括组本身和偏移z轴后的组),将它和之前的那组在y轴上错开,这样动画就能无限衔接了

光照

这里可以自由表现,可以尝试以下几种手法:

  • 漫反射光和镜面反射光相结合
  • 扭曲顶点、法线和uv
  • 根据光线动态计算透明度,以形成玻璃般的效果

后期处理

同样也可以自由表现,可以尝试以下几种手法:

  • RGB扭曲(该特效所采用的)
  • 色差
  • 景深效果
  • 噪声点阵。