// the tangents are computed when the mesh is created, and passed as an attribute
attribute vec3 tangent;
vec3 lookDirection = normalize(position.xyz - cameraPosition);
vec3 offset = cross(lookDirection, tangent);
vec3 adjustedPosition = position.xyz + normalize(offset) * thickness;
要为弧线制作动画,必须计算其当前时间。计算公式为:
# Number of seconds since start of page
uniform float uTime;
# Duration in seconds for arc to travel to destination and disappear
uniform float duration;
# Time when the arc animation should start (relative to page load)
attribute float startTime;
age = clamp((uTime - startTime) / duration, 0.0, 2.0);
float rand(vec2 n) {
return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
float noise(vec2 n) {
const vec2 d = vec2(0.0, 1.0);
vec2 b = floor(n), f = smoothstep(vec2(0.0), vec2(1.0), fract(n));
return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}
float ramp(float t) {
float v = step(0.0, t) * (1.0 - step(1.0, t));
return smoothstep(0.0, 1.0, (1.0 - t)) * v;
}
void main() {
if(age <= 0.0 || age >= 2.0) {
discard;
}
// adjustedAge corresponds to the age of each vertex since the time the arc started
float adjustedAge = age - vUv.y;
float r = ramp(adjustedAge);
vec2 uvScaled = vUv * vNoiseScale;
float n = noise(uvScaled);
float alpha = step(1.0 - r, n);
gl_FragColor = vec4(vColor, alpha);
}
烟花
使用 Three.js 的 IcosahedronGeometry 生成基础网格,并将三角形条带从中心连接到每个顶点。
每条轨迹上的 UV 处理方式与弧线相同,v起点为 0,终点为 1。这使我们能够轻松制作向外爆发的轨迹动画。
if (vUv.y < 0.0 || vUv.y > t) discard;
我们还利用电弧产生的噪声效应来模拟突发随时间消散的过程。这与延迟因子相结合,以便突发轨迹在开始消失之前有一点时间。
float fadeThreshold = smoothstep(0.0, t, vUv.y - delayFactor);
if (noise(vUv) < fadeThreshold) discard;
一旦添加了光晕,整个效果就会真正融合在一起。这是我们第一次添加后期处理效果的地球仪,它增加了我们想要的额外视觉效果。
if (burstTrail) {
// delayDuration is how long between the launch trail finishing and the burst starting
t = (elapsedTime - launchDuration - delayDuration) / burstDuration;
} else {
t = elapsedTime / launchDuration;
}
线性动画 t 从 0 到 1 感觉有点太慢,所以我们尝试了不同的缓动值。立方缓动可以达到我们想要的速度。
float easeOutCubic(float t) {
t = t - 1.0;
return (t * t * t + 1.0);
}
t = easeOutCubic(t);
我们还为发射轨迹高度、爆发大小和旋转偏移添加了一些着色器统一体。这让我们能够以有趣的方式将烟花组合在一起。
最终结果:
完整的顶点着色器:
// vertexShader.glsl
varying float vUvY;
uniform float elapsedTime;
uniform float launchDuration;
uniform float burstDuration;
uniform float delayDuration;
uniform bool isBurstTrail;
void main() {
vec3 pos = position;
float t;
if (isBurstTrail) {
t = (elapsedTime - launchDuration - delayDuration) / burstDuration;
} else {
t = elapsedTime / launchDuration;
}
// 线性动画 t 从 0 到 1
t = clamp(t, 0.0, 1.0);
t = easeOutCubic(t);
// 模拟重力
pos.y -= t * 0.1;
// UV 处理
vUvY = position.y;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
float easeOutCubic(float t) {
t = t - 1.0;
return (t * t * t + 1.0);
}
片元着色器:
// fragmentShader.glsl
varying float vUvY;
uniform float t;
uniform float delayFactor;
void main() {
// UV 处理
if (vUvY < 0.0 || vUvY > t) discard;
// 模拟突发随时间消散的过程
float fadeThreshold = smoothstep(0.0, t, vUvY - delayFactor);
if (noise(vUvY) < fadeThreshold) discard;
// 设置颜色
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); // 黄色
}
烟花组件:
// src/Fireworks.js
import React, { useRef, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { ShaderMaterial, IcosahedronGeometry } from 'three';
import vertexShader from './vertexShader.glsl';
import fragmentShader from './fragmentShader.glsl';
const Firework = ({ elapsedTime, launchDuration, burstDuration, delayDuration }) => {
const meshRef = useRef();
const isBurstTrail = useRef(false);
useFrame(() => {
if (meshRef.current) {
meshRef.current.material.uniforms.elapsedTime.value = elapsedTime;
meshRef.current.material.uniforms.launchDuration.value = launchDuration;
meshRef.current.material.uniforms.burstDuration.value = burstDuration;
meshRef.current.material.uniforms.delayDuration.value = delayDuration;
meshRef.current.material.uniforms.isBurstTrail.value = isBurstTrail.current;
}
});
return (
<mesh ref={meshRef}>
<icosahedronGeometry args={[1, 0]} />
<shaderMaterial
vertexShader={vertexShader}
fragmentShader={fragmentShader}
uniforms={{
elapsedTime: { value: 0 },
launchDuration: { value: launchDuration },
burstDuration: { value: burstDuration },
delayDuration: { value: delayDuration },
isBurstTrail: { value: isBurstTrail.current },
}}
/>
</mesh>
);
};
const FireworksCanvas = () => {
const [elapsedTime, setElapsedTime] = React.useState(0);
useEffect(() => {
const interval = setInterval(() => {
setElapsedTime((prev) => prev + 0.1);
}, 100);
return () => clearInterval(interval);
}, []);
return (
<Canvas>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<Firework elapsedTime={elapsedTime} launchDuration={2} burstDuration={1} delayDuration={0.5} />
</Canvas>
);
};
export default FireworksCanvas;
文章来源:
https://shopify.engineering/how-we-built-shopifys-bfcm-2023-globe