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个函数createPolygonShape和polySort:前者能接收一系列的点来创造一个多边形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扭曲(该特效所采用的)
  • 色差
  • 景深效果
  • 噪声点阵。
详解PHP中数组函数的知识点 网站建设

详解PHP中数组函数的知识点

PHP 的数组是一种很强大的数据类型,与此同时 PHP 内置了一系列与数组相关的函数可以很轻易的实现日常开发的功能。所以本文便总结了一些在常见场景中利用 PHP 内置函数的实现方法,希望对你有所帮助!...
打印Proxy对象和ref对象的包实现详解 网站建设

打印Proxy对象和ref对象的包实现详解

平时工作的时候,我喜欢用console.log调试大法。但Vue3更新后,控制台都是打印的Proxy对象和ref对象,想看里边的值,就需要很麻烦的一层一层的展开。 为了解决这个问题,我试过在编辑...