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

目录
  • 效果图
  • 建模
    • 多边形形状
    • 随机多边形
  • 漂浮动画
  • 光照
  • 后期处理

效果图

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

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

<div id="sketch"></div>
<div>
    <div class="fixed z-5 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-3xl tracking-widest whitespace-no-wrap">
                <span style="--i: 0">L</span>
                <span style="--i: 1">O</span>
                <span style="--i: 2">A</span>
                <span style="--i: 3">D</span>
                <span style="--i: 4">I</span>
                <span style="--i: 5">N</span>
                <span style="--i: 6">G</span>
            </div>
        </div>
    </div>
    <div class="fixed z-4 top-0 left-0 w-screen h-screen text-white text-xl">
        <div class="absolute hv-center">
            <div class="scene-1 space-y-10 text-center whitespace-no-wrap">
                <div class="space-y-4">
                    <div class="shuffle-text shuffle-text-1">欢迎来到时空裂缝!</div>
                    <div class="shuffle-text shuffle-text-2">在这里,你可以体验穿梭时空的感觉!</div>
                    <div class="shuffle-text shuffle-text-3">准备好了,就点击下面的按钮吧~</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-2 space-y-10 text-center whitespace-no-wrap">
                <div class="space-y-4">
                    <div class="shuffle-text shuffle-text-4">穿梭的感觉如何?</div>
                    <div class="shuffle-text shuffle-text-5">如果觉得不错,可以推荐给其他小伙伴~</div>
                    <div class="shuffle-text shuffle-text-6">我是alphardex,一个爱写特效的前端</div>
                </div>
            </div>
        </div>
    </div>
</div>

CSS

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

JS

const vertexShader = `
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vMvPosition;
varying vec3 vPosition;
uniform vec2 uMouse;
uniform float uRandom;
uniform float uLayerId;
// transform
mat2 rotation2d(float angle){
    float s=sin(angle);
    float c=cos(angle);
    return mat2(
        c,-s,
        s,c
    );
}
mat4 rotation3d(vec3 axis,float angle){
    axis=normalize(axis);
    float s=sin(angle);
    float c=cos(angle);
    float oc=1.-c;
    return mat4(
        oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,0.,
        oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
        oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0.,
        0.,0.,0.,1.
    );
}
vec2 rotate(vec2 v,float angle){
    return rotation2d(angle)*v;
}
vec3 rotate(vec3 v,vec3 axis,float angle){
    return(rotation3d(axis,angle)*vec4(v,1.)).xyz;
}
vec3 distort(vec3 p){
    vec3 tx1=vec3(-uMouse.x*uRandom*.05,-uMouse.y*uRandom*.02,0.);
    p+=tx1;
    float angle=iTime*uRandom;
    p=rotate(p,vec3(0.,1.,0.),angle);
    vec3 tx2=vec3(-uMouse.x*uRandom*.5,-uMouse.y*uRandom*.2,0.);
    p+=tx2;
    p*=(.6-uLayerId*.5);
    return p;
}
void main(){
    vec3 p=position;
    vec3 N=normal;
    p=distort(p);
    N=distort(N);
    gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
    vUv=uv;
    vNormal=N;
    vMvPosition=modelViewMatrix*vec4(p,1.);
    vPosition=p;
}
`;
const fragmentShader = `
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
varying vec2 vUv;
varying vec3 vNormal;
varying vec4 vMvPosition;
varying vec3 vPosition;
uniform sampler2D uTexture;
uniform vec3 uLightPosition;
uniform vec3 uLightColor;
uniform float uRandom;
uniform vec2 uMouse;
// transform
mat2 rotation2d(float angle){
    float s=sin(angle);
    float c=cos(angle);
    return mat2(
        c,-s,
        s,c
    );
}
mat4 rotation3d(vec3 axis,float angle){
    axis=normalize(axis);
    float s=sin(angle);
    float c=cos(angle);
    float oc=1.-c;
    return mat4(
        oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,0.,
        oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
        oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0.,
        0.,0.,0.,1.
    );
}
vec2 rotate(vec2 v,float angle){
    return rotation2d(angle)*v;
}
vec3 rotate(vec3 v,vec3 axis,float angle){
    return(rotation3d(axis,angle)*vec4(v,1.)).xyz;
}
// lighting
float saturate(float a){
    return clamp(a,0.,1.);
}
float diffuse(vec3 n,vec3 l){
    float diff=saturate(dot(n,l));
    return diff;
}
float specular(vec3 n,vec3 l,float shininess){
    float spec=pow(saturate(dot(n,l)),shininess);
    return spec;
}
float blendSoftLight(float base,float blend){
    return(blend<.5)?(2.*base*blend+base*base*(1.-2.*blend)):(sqrt(base)*(2.*blend-1.)+2.*base*(1.-blend));
}
vec3 blendSoftLight(vec3 base,vec3 blend){
    return vec3(blendSoftLight(base.r,blend.r),blendSoftLight(base.g,blend.g),blendSoftLight(base.b,blend.b));
}
// distort
vec2 distort(vec2 p){
    vec2 m=uMouse;
    p.x-=(uRandom-m.x*.8)*.5;
    p.y-=uRandom*.1-iTime*.1;
    p.x-=.25;
    p.y-=.5;
    p=rotate(p,uRandom);
    p*=2.;
    return p;
}
vec3 distortNormal(vec3 p){
    p*=vec3(-1.*uRandom*15.,-1.*uRandom*15.,30.5);
    return p;
}
// lighting
vec4 lighting(vec3 tex,vec3 normal){
    vec4 viewLightPosition=viewMatrix*vec4(uLightPosition,0.);
    vec3 N=normalize(normal);
    vec3 L=normalize(viewLightPosition.xyz);
    vec3 dif=tex*uLightColor*diffuse(N,L);
    vec3 C=-normalize(vMvPosition.xyz);
    vec3 R=reflect(-L,N);
    vec3 spe=uLightColor*specular(R,C,500.);
    vec4 lightingColor=vec4(dif+spe,.5);
    vec3 softlight=blendSoftLight(tex,spe);
    float dotRC=dot(R,C);
    float theta=acos(dotRC/length(R)*length(C));
    float a=1.-theta*.3;
    vec4 col=vec4(tex,a*.01)+vec4(softlight,.02)+(lightingColor*a);
    return col;
}
void main(){
    vec2 p=vUv;
    vec3 N=vNormal;
    p=distort(p);
    N=distortNormal(N);
    vec4 tex=texture2D(uTexture,p);
    vec4 col=tex;
    col=lighting(tex.xyz,N);
    gl_FragColor=col;
}
`;
const vertexShader2 = `
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
varying vec2 vUv;
void main(){
    vec3 p=position;
    gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
    vUv=uv;
}
`;
const fragmentShader2 = `
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
uniform sampler2D tDiffuse;
varying vec2 vUv;
uniform float uRGBShift;
vec4 RGBShift(sampler2D t,vec2 rUv,vec2 gUv,vec2 bUv){
    vec4 color1=texture2D(t,rUv);
    vec4 color2=texture2D(t,gUv);
    vec4 color3=texture2D(t,bUv);
    vec4 color=vec4(color1.r,color2.g,color3.b,color2.a);
    return color;
}
highp float random(vec2 co)
{
    highp float a=12.9898;
    highp float b=78.233;
    highp float c=43758.5453;
    highp float dt=dot(co.xy,vec2(a,b));
    highp float sn=mod(dt,3.14);
    return fract(sin(sn)*c);
}
void main(){
    vec2 p=vUv;
    vec4 col=vec4(0.);
    // RGB Shift
    float n=random(p+mod(iTime,1.))*.1+.5;
    vec2 offset=vec2(cos(n),sin(n))*.0025*uRGBShift;
    vec2 rUv=p+offset;
    vec2 gUv=p;
    vec2 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(0.1, 0.1, 16, 16);
    const shape = kokomi.createPolygonShape(this.points, {
      scale: 0.01,
    });
    const geometry = new THREE.ExtrudeGeometry(shape, {
      steps: 1,
      depth: 0.0001,
      bevelEnabled: true,
      bevelThickness: 0.0005,
      bevelSize: 0.0005,
      bevelSegments: 1,
    });
    geometry.center();
    const matClone = material.clone();
    matClone.uniforms.uRandom.value = THREE.MathUtils.randFloat(0.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 * 0.01;
    this.mesh.material.uniforms.uLightPosition.value.copy(
      new THREE.Vector3(Math.cos(lp), Math.sin(lp), 10)
    );
  }
}
class FragmentGroup extends kokomi.Component {
  constructor(base, config = {}) {
    super(base);
    const { material, layerId = 0, 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[0];
      frag.mesh.position.set(
        firstPoint.x * 0.01,
        firstPoint.y * -0.01,
        THREE.MathUtils.randFloat(-3, -1)
      );
      frag.mesh.material.uniforms.uLayerId.value = layerId;
      g.add(frag.mesh);
      return frag;
    });
    this.g.position.z = 2 - 1.5 * layerId;
    this.frags = frags;
  }
  addExisting() {
    this.base.scene.add(this.g);
  }
}
const generatePolygons = (config = {}) => {
  const { gridX = 10, gridY = 20, maxX = 9, maxY = 9 } = config;
  const polygons = [];
  for (let i = 0; i < gridX; i++) {
    for (let j = 0; j < gridY; j++) {
      const points = [];
      let edgeCount = 3;
      const randEdgePossibility = Math.random();
      if (randEdgePossibility > 0 && randEdgePossibility <= 0.2) {
        edgeCount = 3;
      } else if (randEdgePossibility > 0.2 && randEdgePossibility <= 0.55) {
        edgeCount = 4;
      } else if (randEdgePossibility > 0.55 && randEdgePossibility <= 0.9) {
        edgeCount = 5;
      } else if (randEdgePossibility > 0.9 && randEdgePossibility <= 0.95) {
        edgeCount = 6;
      } else if (randEdgePossibility > 0.95 && randEdgePossibility <= 1) {
        edgeCount = 7;
      }
      let firstPoint = {
        x: 0,
        y: 0,
      };
      let angle = THREE.MathUtils.randFloat(0, 2 * Math.PI);
      for (let k = 0; k < edgeCount; k++) {
        if (k === 0) {
          firstPoint = {
            x: (i % maxX) * 10,
            y: (j % maxY) * 10,
          };
          points.push(firstPoint);
        } else {
          // random polar
          const r = 10;
          angle += THREE.MathUtils.randFloat(0, 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.Vector3(-0.36, 0.36, 0.1));
    // fragment groups
    const polygons = generatePolygons();
    const fgs = [...Array(2).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 loop
    const fgsContainer2 = new THREE.Group().copy(fgsContainer.clone());
    fgsContainer2.position.y = fgsContainer.position.y - 1;
    const totalG = new THREE.Group();
    totalG.add(fgsContainer);
    totalG.add(fgsContainer2);
    this.totalG = totalG;
    // anime
    this.floatDistance = 0;
    this.floatSpeed = 1;
    this.floatMaxDistance = 1;
    this.isDashing = false;
  }
  addExisting() {
    this.base.scene.add(this.totalG);
  }
  update() {
    this.floatDistance += this.floatSpeed;
    const y = this.floatDistance * 0.001;
    if (y > this.floatMaxDistance) {
      this.floatDistance = 0;
    }
    if (this.totalG) {
      this.totalG.position.y = y;
    }
  }
  speedUp() {
    gsap.to(this, {
      floatSpeed: 50,
      duration: 4,
      ease: "power2.in",
    });
  }
  speedDown() {
    gsap.to(this, {
      floatSpeed: 1,
      duration: 6,
      ease: "power3.inOut",
    });
  }
  async dash(duration = 5000, 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, 0, 1.5);
    this.camera.fov = 10;
    this.camera.near = 0.01;
    this.camera.far = 10000;
    this.camera.updateProjectionMatrix();
    const resourceList = [
      {
        name: "tex1",
        type: "texture",
        path: "https://s2.loli.net/2022/11/19/cqOho3ZKCXfTdzw.jpg",
      },
      {
        name: "tex2",
        type: "texture",
        path: "https://s2.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["tex1"];
      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.Vector3(-0.2, -0.2, 3),
          },
          uLightColor: {
            value: new THREE.Color("#eeeeee"),
          },
          uRandom: {
            value: THREE.MathUtils.randFloat(0.1, 1.1),
          },
          uMouse: {
            value: new THREE.Vector2(0.5, 0.5),
          },
          uLayerId: {
            value: 0,
          },
        },
      });
      // fragment world
      const fw = new FragmentWorld(this, {
        material,
      });
      fw.addExisting();
      this.fw = fw;
      // postprocessing
      const ce = new kokomi.CustomEffect(this, {
        vertexShader: vertexShader2,
        fragmentShader: fragmentShader2,
        uniforms: {
          uRGBShift: {
            value: 1,
          },
        },
      });
      ce.addExisting();
      // DOM
      const 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(500);
        gsap.to("#sketch", {
          opacity: 1,
        });
        await kokomi.sleep(1000);
        await startScene1();
      };
      const startScene2 = async () => {
        gsap.set(".scene-2", {
          display: "block",
        });
        shuffleText(".shuffle-text-4");
        await kokomi.sleep(1000);
        shuffleText(".shuffle-text-5");
        await kokomi.sleep(1000);
        shuffleText(".shuffle-text-6");
        await kokomi.sleep(6000);
        gsap.to(".scene-2", {
          opacity: 0,
          pointerEvents: "none",
        });
      };
      const startScene1 = async () => {
        gsap.set(".scene-1", {
          display: "block",
        });
        shuffleText(".shuffle-text-1");
        await kokomi.sleep(1000);
        shuffleText(".shuffle-text-2");
        await kokomi.sleep(1000);
        shuffleText(".shuffle-text-3");
        await kokomi.sleep(1000);
        gsap.to(".dash-btn", {
          opacity: 1,
          pointerEvents: "auto",
        });
        document
          .querySelector(".dash-btn")
          .addEventListener("click", async () => {
            gsap.set(".dash-btn", {
              pointerEvents: "none",
            });
            gsap.to(".scene-1", {
              opacity: 0,
              pointerEvents: "none",
              display: "none",
            });
            await this.fw.dash(5000, () => {
              this.fw.changeTexture("tex2");
            });
            await kokomi.sleep(5000);
            await startScene2();
          });
      };
      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: 0, y: 0 },
  { x: 25, y: 0 },
  { x: 45, y: 45 },
  { x: 0, y: 25 },
];
points = kokomi.polySort(points);
const shape = kokomi.createPolygonShape(points, {
  scale: 0.01,
});
const geometry = new THREE.ExtrudeGeometry(shape, {
  steps: 1,
  depth: 0.0001,
  bevelEnabled: true,
  bevelThickness: 0.0005,
  bevelSize: 0.0005,
  bevelSegments: 1,
});
geometry.center();

随机多边形

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

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

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

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

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

光照

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

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

后期处理

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

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

以上就是three.js创造时空裂缝特效的详细内容,更多关于three.js 时空裂缝特效的资料请关注我们其它相关文章!

(0)

相关推荐

  • Three.js Interpolant实现动画插值

    目录 Interpolant 通过离散的采样点定义曲线 插值的步骤 1. 寻找要插值的位置 2. 根据找到的左右两个点,进行插值 Interpolant源码 1. 构造器 2. copySampleValue_() 3. interpolate_( /* i1, t0, t, t1 */ ) 4. evaluate() 5. LinearInterpolant实现interpolate_( /* i1, t0, t, t1 */ )方法 总结 Interpolant 这个类主要是用来实现插值,常

  • Three.js利用dat.GUI如何简化试验流程详解

    简介 本文主要给大家介绍了关于Three.js利用dat.GUI如何简化试验流程的想内容,其实使用这个插件的最省事的地方在于,调试很方便的调节相关的值,从而影响最后绘制的结果.而dat.GUI实现的东西也很简单,理解起来也很好理解.下面话不多说了,来一起看看详细的介绍吧. 我们实例化dat.GUI对象后,会在右上角显示出一些可以调节的参数,比如: 这就是今天的案例制作出来的五个可以调节的属性.而且实现起来也很简单,而且大部分是需要我们做的,除了上面的这个控制台不是我们写出来的. 引入方式 首先,

  • three.js镜头追踪的移动效果实例

    目录 达到效果 实现思路 实现难点 1.折现变曲线 2.镜头朝向不受控 3.镜头位置绑定不受控 4.镜头抖动 最终实现方法 方法一:镜头沿线推进 方法二:使用tween动画 方法比较 其他方法 方法一:绘制一条折线+animate镜头推进 方法二:绘制多条线段+animate镜头推进 方法三:绘制多条线段+tween动画变化镜头 方法四:优化方法一,绘制一条折线+animate镜头推进 达到效果 指定一条折线路径,镜头沿着路径向前移动,类似第一视角走在当前路径上. 实现思路 很简单画一条折线路径

  • vue+three.js实现炫酷的3D登陆页面示例详解

    目录 前言: Three.js的基础知识 关于场景 关于光源 关于相机(重要) 关于渲染器 完善效果 创建一个左上角的地球 使地球自转 创建星星 使星星运动 创建云以及运动轨迹 使云运动 完成three.js有关效果 结语 前言: 大家好,我是xx传媒严导(xx这两个字请自行脑补) . 该篇文章用到的主要技术:vue3.three.js 我们先看看成品效果: 高清大图预览(会有些慢): 座机小图预览: 废话不多说,直接进入正题 Three.js的基础知识 想象一下,在一个虚拟的3D世界中都需要什

  • Three.js引入Cannon.js及使用示例详解

    目录 引言 大体代码及效果 Cannon.js 打造当前 UI 引言 在开始之前,我们还是要解释下什么是 Cannon.js 以及它的作用. Cannon.js 是一个 3D 物理引擎,通过为物体赋予真实的物理属性的方式来计算运动.旋转和碰撞检测.Cannon.js 相较于其他常见的物理引擎来说,比较轻量级而且完全通过 JavaScript 来实现. Cannon.js 的官方文档地址为 schteppe.github.io/cannon.js/ ,从官方介绍中也可以看到很多有趣的例子,如下所示

  • three.js-结合dat.gui实现界面可视化修改及调试详解

    目录 gitee 地址 本篇目标 下载 dat.gui 引入及创建 引入 创建 使用 效果图 完整代码 gitee 地址 gitee地址: gittee地址 本篇目标 结合dat.gui实现界面可视化修改及调试 下载 dat.gui pnpm i dat.gui 引入及创建 引入 import * as Dat from "dat.gui"; 创建 /// 创建 dat const dat = new Dat.GUI(); 使用 这里我是装在一个函数里的,但不影响 /// 添加菜单 f

  • 用p5.js制作烟花特效的示例代码

    前言 之前看过一篇文章,使用processing制作烟花特效.效果如下 fireworks 网上调查了一圈了,发现processing是一个互动编程软件,java语言发展而来.而且动画效果是跑在processing专门的模拟器上. 不过好在也有对应的web扩展语言,有processing.js和p5.js. processing.js在github上已经好几年没有人维护了,一些processing的特性支持不了.为此踩了不少坑, 本文就集中讲解如何用p5.js写烟花特效. 代码讲解 proces

  • js实现星星海特效的示例

    首先需要获取屏幕大小: var screenWidth = document.documentElement.clientWidth; var screenHeight = document.documentElement.clientHeight; 接着可以定义动画(星星透明度): @keyframes flash { 0%{opacity: 0} 25%{opacity: 0.25} 50%{opacity: 0.5} 75%{opacity: 0.75} 100%{opacity: 1}

  • 基于JS实现动态跟随特效的示例代码

    目录 演示 技术栈 源码 css部分 js部分 演示 技术栈 这次用到了关于css的一些功能,和jQuery. CSS3中添加的新属性animation是用来为元素实现动画效果的,但是animation无法单独担当起实现动画的效果.承载动画的另一个属性——@keyframes.使用的时候为了兼容可加上-webkit-.-o-.-ms-.-moz-.-khtml-等前缀以适应不同的浏览器. 创建动画的原理是,将一套 CSS 样式逐渐变化为另一套样式. 通过 @keyframes 规则,您能够创建动

  • js变形金刚文字特效代码分享

    为大家分享的js变形金刚文字特效代码如下 -----------------------------------------------效果演示----------------------------------------------- <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>js变形金刚文字特效&l

  • vue 粒子特效的示例代码

    本文介绍了vue 粒子特效的示例代码,分享给大家,具体如下: 实现效果: 没错,你看到的上图那些类似于星座图的点和线,是由vue-particles生成的,而且能与用户鼠标事件产生互动. 传送门:vue-particles 使用教程 npm install vue-particles --save-dev main.js文件: import Vue from 'vue' import VueParticles from 'vue-particles' Vue.use(VueParticles)

  • Android 使用jQuery实现item点击显示或隐藏的特效的示例

    本文介绍了Android 使用jQuery实现item点击显示或隐藏的特效的示例,分享给大家,具体如下: 效果图 分析 上图中的功能在很多APP上都可能用到过,例如app的帮助界面,告诉用户如何使用APP 一般的实现方式都是通过ListView来实现的,实际上此类需求非常简单,完全可以用WebView加载HTML来实现 抽屉样式的显示隐藏特效可以使用jQuery来实现,一个函数就可以搞定 实现 集成jQuery 网页中需要使用到jQuery,最新版本可以去官网下载 http://jquery.c

  • 使用electron制作满屏心特效的示例代码

    本文介绍了使用electron制作满屏心特效的示例代码,分享给大家,具体如下: 图片被压缩了 看起来很难看 主进程代码 import {BrowserWindow, app, ipcMain} from 'electron' createWindow(); ipcMain.on('quitApp', () => { app.quit(); }); function createWindow() { const loginURL = process.env.NODE_ENV === 'develo

  • 利用JS做网页特效_大图轮播(实例讲解)

    废话不多说,直接上代码: <style> * { margin: 0px; padding: 0px; } .stage { width: 500px; height: 300px; border: 5px solid black; margin: 200px; position: relative; overflow: hidden; } .to-left, .to-right { position: absolute; top: 0px; width: 50px; height: 300p

  • JS实现按钮添加背景音乐示例代码

    1-代码 <html> <head> <meta charset="utf-8"> <title>js实现按键声</title> </head> <body> <ul> <li> <a href="menu-list" rel="external nofollow" rel="external nofollow"

  • JS鼠标滚动分页效果示例

    首先先看问题: 在开发的时候,看到这种现象 就会思考:为什么左边的数据出来比右边的慢呢?因为这里没有进行分页,左边的数据多,所以查询相对较慢. 解决办法就是进行分页,但是在项目中用到的插件,不能控制样式,改变分页的宽度,样式就会乱掉.最简单的办法就是不分页(^_^) 但是既然是自己份内的事,为啥不做好呢?那就写分页呗,滚动分页! 问了其他同事,其他同事也...你去百度去.... 是啊,网上一大堆 ,但都是乱七八糟的,也没有效果图...坑 经过一番思考,和百度 思路来了: 需要了解三个dom元素,

随机推荐