目录
一、创建光带
(1) 设置光带顶点
(2) 设置光带顶点透明度属性
二、光带动画
完整代码
html文件代码
js文件代码
最后展示一下项目里的效果:
最近项目中要求做一段光带效果动画,尝试着写了一下,下面是本次分享光带扩散动画的效果预览:
20240110_204035
一、创建光带
(1) 设置光带顶点
这里使用缓冲区几何体bufferGeometry,通过设置顶点属性position来构成光带模型,在创建顶点之前需要一下几个必备参数:
r | 光带初始时的半径 |
h | 光带的高度 |
radian | 弧度值 |
segment | 间隔段数,光带由N段矩形构成(矩形由2个三角形构建),此属性决定矩形数量,值越大光带越接近圆形 |
interval | 每段间隔的弧度值 |
// 创建缓冲区几何体
const geometry = new THREE.BufferGeometry();
// 光带初始半径
const r = 10;
// 光带高度
const h = 10;
// 弧度
let radian = 0;
// 间隔段数,此值越高光带棱角越分明
const segment = 50;
// 弧度间隔
const interval = (Math.PI * 2) / segment;
接下来就是创建光带的顶点位置数组了,光带由N个矩形组成,一个矩形又由两个三角形构成;
for循环遍历间隔段数segment,每3个值代表一个顶点位置,3个顶点位置又组成一个三角形;
x轴上的位置使用Math.cos函数得出,z轴上的位置使用Math.sin函数得出,y轴则看三角形的三个点创建顺序来得出。此处我创建点位时,三角形底下的点为点2,所以点2的y值设置为0
第一个三角形点位顺序(第二个三角形类推,这里不展示了):
最后通过bufferAttribute属性设置几何体顶点位置,注意顶点位置数组需要转换成32位浮点类型的数组
// 顶点位置数组
const vertexPosArr = [];
// 遍历出光带的顶点数据
for (let i = 0; i < segment; i++) {
// 弧度逐渐增加,从0度增加到360度
radian += interval;
// 计算出两个三角形的顶点位置,形成一个矩形平面,最后多个矩形平面组成圆形的光带
vertexPosArr.push(
// 第一个三角形
Math.cos(radian) * r, h, Math.sin(radian) * r, // 点1
Math.cos(radian) * r, 0, Math.sin(radian) * r, // 点2
Math.cos(radian + interval) * r, 0, Math.sin(radian + interval) * r, // 点3
// 第二个三角形
Math.cos(radian) * r, h, Math.sin(radian) * r, // 点1
Math.cos(radian + interval) * r, 0, Math.sin(radian + interval) * r, // 点2
Math.cos(radian + interval) * r, h, Math.sin(radian + interval) * r, // 点3
)
}
// 设置几何体缓冲区position属性
geometry.attributes.position = new THREE.BufferAttribute(new Float32Array(vertexPosArr), 3);
(2) 设置光带顶点透明度属性
光带是渐变透明的,由黑到白(效果中蓝色是因为材质设置了蓝色将白色替换了);
通过获取顶点的getY函数获取当前顶点的y值(也就是顶点的高度),(1-顶点高度) / 光带高度使光带从下往上逐渐透明,也可以换成顶点高度 / 光带高度使光带从上往下逐渐透明
// 设置几何体缓冲区position属性
geometry.attributes.position = new THREE.BufferAttribute(new Float32Array(vertexPosArr), 3);
// 获取顶点
const position = geometry.attributes.position;
// 顶点总数量
const count = position.count;
// 透明度数组,每个顶点位置将会对应一个透明度
const alphaArr = [];
// 根据高度设置顶点透明度
for (let i = 0; i < count; i++) {
alphaArr.push((1 - position.getY(i) / h));
}
// 设置几何体缓冲区alpha属性
geometry.attributes.alpha = new THREE.BufferAttribute(new Float32Array(alphaArr), 1);
(3) 创建光带材质
这里使用的普通网格材质,这里必须设置side属性为THREE.DoubleSide双面可见、材质透明度transparent属性开启
至于材质使用onBeforeCompile函数替换着色器shader代码一块这里不做说明了,因为这一块东西很多,一时也说不清楚。
// 创建光带的材质
const material = new THREE.MeshBasicMaterial({
color: '#00ffff',
side: THREE.DoubleSide,
transparent: true,
depthTest: false,
})
// 材质渲染前所执行,替换shader着色器代码
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
'void main() {',
`
// 引进透明度分量
attribute float alpha;
// varying声明一个属性,赋值透明度分量alpha,让片元着色器能拿到这个属性
varying float vAlpha;
void main() {
vAlpha = alpha;
`,
)
shader.fragmentShader = shader.fragmentShader.replace(
'void main() {',
`
// 引进从顶点着色器传递的透明度分量
varying float vAlpha;
void main() {
`,
)
shader.fragmentShader = shader.fragmentShader.replace(
'#include <output_fragment>',
`
#include <output_fragment>
// 设置颜色和透明度值,让光带有一个渐变效果
gl_FragColor = vec4( outgoingLight, vAlpha );
`,
)
}
const lightBand = new THREE.Mesh(geometry, material);
scene.add(lightBand);
二、光带动画
这里的光带动画是写在循环执行函数内的,有过threejs基础一定不陌生;
这里每次使用clone属性克隆光带模型获取scale的x值(换成z值也一样,y值不可以),通过if判断光带当前缩放大小来决定相应操作;
这里光带将从1倍扩散到9倍,7倍到9倍的时候会逐渐减小光带高度,这有就又了光带扩散动画末尾的逐渐消失效果,最后超过9倍重置缩放倍数为1,形成循环;
// 渲染循环
function render () {
// 光带当前缩放倍数
let scale = lightBand.clone().scale.x;
// 小于7时scale不断增加
if (scale < 7) {
scale += 0.02;
// 重新设置光带缩放倍数
lightBand.scale.set(scale, 1, scale);
}
// 小于8时scale不断增加,但是光带高度逐渐减小
else if (scale < 9) {
scale += 0.02;
lightBand.scale.set(scale, (9 - scale) / 2, scale);
}
// 大于9时光带缩放倍数重置为1
else {
scale = 0;
lightBand.scale.set(scale, scale, scale);
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
完整代码
这里使用的html+js构建的小案例,threejs使用的148的版本
html文件代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
body {
overflow: hidden;
margin: 0;
}
</style>
<body>
<div id="webgl"></div>
<script type="importmap">
{
"imports":{
"three":"../../build/three.module.js",
"three/addons/": "../../examples/jsm/"
}
}
</script>
<script src="./index.js" type="module"></script>
</body>
</html>
js文件代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const width = window.innerWidth;
const height = window.innerHeight;
// 创建场景
const scene = new THREE.Scene();
// 设置光源
const pointLight = new THREE.PointLight('#ffffff', 1, 0);
pointLight.position.set(200, 0, 200);
scene.add(pointLight);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);
// 创建透视相机
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
const planeGeometry = new THREE.PlaneGeometry(200, 200);
const planeMaterial = new THREE.MeshBasicMaterial({ color: '#696969' });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotateX(-Math.PI / 2);
scene.add(plane);
// 创建缓冲区几何体
const geometry = new THREE.BufferGeometry();
// 光带初始半径
const r = 10;
// 光带高度
const h = 10;
// 弧度
let radian = 0;
// 间隔段数,此值越高光带棱角越分明
const segment = 50;
// 弧度间隔
const interval = (Math.PI * 2) / segment;
// 顶点位置数组
const vertexPosArr = [];
// 遍历出光带的顶点数据
for (let i = 0; i < segment; i++) {
// 弧度逐渐增加,从0度增加到360度
radian += interval;
// 计算出两个三角形的顶点位置,形成一个矩形平面,最后多个矩形平面组成圆形的光带
vertexPosArr.push(
// 第一个三角形
Math.cos(radian) * r, h, Math.sin(radian) * r, // 点1
Math.cos(radian) * r, 0, Math.sin(radian) * r, // 点2
Math.cos(radian + interval) * r, 0, Math.sin(radian + interval) * r, // 点3
// 第二个三角形
Math.cos(radian) * r, h, Math.sin(radian) * r, // 点1
Math.cos(radian + interval) * r, 0, Math.sin(radian + interval) * r, // 点2
Math.cos(radian + interval) * r, h, Math.sin(radian + interval) * r, // 点3
)
}
// 设置几何体缓冲区position属性
geometry.attributes.position = new THREE.BufferAttribute(new Float32Array(vertexPosArr), 3);
// 获取顶点数
const position = geometry.attributes.position;
// 顶点总数量
const count = position.count;
// 透明度数组,每个顶点位置将会对应一个透明度
const alphaArr = [];
// 根据高度设置顶点透明度
for (let i = 0; i < count; i++) {
const temp = 1 - position.getY(i) / h;
alphaArr.push(temp);
console.log(temp, position.getY(i))
}
// 设置几何体缓冲区alpha属性
geometry.attributes.alpha = new THREE.BufferAttribute(new Float32Array(alphaArr), 1);
// 创建光带的材质
const material = new THREE.MeshBasicMaterial({
color: '#00ffff',
side: THREE.DoubleSide,
transparent: true,
depthTest: false,
})
// 材质渲染前所执行,替换shader着色器代码
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
'void main() {',
`
// 引进透明度分量
attribute float alpha;
// varying声明一个属性,赋值透明度分量alpha,让片元着色器能拿到这个属性
varying float vAlpha;
void main() {
vAlpha = alpha;
`,
)
shader.fragmentShader = shader.fragmentShader.replace(
'void main() {',
`
// 引进从顶点着色器传递的透明度分量
varying float vAlpha;
void main() {
`,
)
shader.fragmentShader = shader.fragmentShader.replace(
'#include <output_fragment>',
`
#include <output_fragment>
// 设置颜色和透明度值,让光带有一个渐变效果
gl_FragColor = vec4( outgoingLight, vAlpha );
`,
)
}
const lightBand = new THREE.Mesh(geometry, material);
scene.add(lightBand);
// 渲染循环
function render () {
// 光带当前缩放倍数
let scale = lightBand.clone().scale.x;
// 小于7时scale不断增加
if (scale < 7) {
scale += 0.02;
// 重新设置光带缩放倍数
lightBand.scale.set(scale, 1, scale);
}
// 小于8时scale不断增加,但是光带高度逐渐减小
else if (scale < 9) {
scale += 0.02;
lightBand.scale.set(scale, (9 - scale) / 2, scale);
}
// 大于9时光带缩放倍数重置为1
else {
scale = 0;
lightBand.scale.set(scale, scale, scale);
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
// 创建相机轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', () => {
renderer.render(scene, camera);
})
// 设置界面跟随窗口自适应
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
最后展示一下项目里的效果:
案例中如有不足的请补充,不懂的也可以问我,我知道的会尽力解答