目录
- 效果图
- 建模
- 多边形形状
- 随机多边形
- 漂浮动画
- 光照
- 后期处理
效果图
最近受到轮回系作品《寒蝉鸣泣之时》中时空裂缝场景的启发,我用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个函数createPolygonShape和polySort:前者能接收一系列的点来创造一个多边形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扭曲(该特效所采用的)
- 色差
- 景深效果
- 噪声点阵。