Three.js实现文字雨效果,文字垂直掉落

本文目标是实现如下文字雨垂直降落的效果,如下动图:

代码仓库:

1. 功能概览

  1. 实现文字雨垂直下落,下降速度不同,有远近的效果
  2. 底部有同心圆,同心圆会发生旋转
  3. 插入文字标签,并跟随同心圆一起旋转
  4. 双击暂停动画,再次双击继续开始动画

2. 基础架构搭建

  1. 在vite或者vue项目中开发都可以,我是在vite项目中开发

  2. 必须安装好three.js的npm包

  3. main.js中引入three

js 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
  1. 场景、摄像机、渲染器、轨道控制器
js 复制代码
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdec9aa)

// 创建相机
const camera = new THREE.PerspectiveCamera(
  45, // 视角
  window.innerWidth / window.innerHeight, // 宽高比
  0.01, // 近平面
  100 // 远平面
);

// 设置相机位置
camera.position.z = 50;
camera.position.y = 2.5;
camera.position.x = 3;
camera.lookAt(0, 1.2, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 开启抗锯齿
});
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping
renderer.toneMappingExposure = 1
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加轨道控制器,注意轨道控制器不需要加入到场景里面去
const controls = new OrbitControls(camera, renderer.domElement);
// 设置带阻尼的惯性
controls.enableDamping = true;
// 设置阻尼系数
controls.dampingFactor = 0.05;
// 设置旋转速度
// controls.autoRotate = true;
controls.zoom = 20

3. 文字雨效果

  1. 准备数据源,要下降的文字,这个数据一般要从接口拿到
js 复制代码
const groupsTexts = [
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞",
    "生而不有",
    "是以圣人处无为之事",
    "难易想成",
    "皆知善之为善",
    "功成而弗居",
    "是以不去",
    "长短相较",
    "行不言之教",
    "天下皆知美之为美",
    "长短相教",
    "万物作焉而不辞"
]
  1. createTextSprite创建一个精灵字体
js 复制代码
// 创建文字材质函数,基于canvas画布生成纹理
function createTextSprite(text, color='white', opacity = 1) {
    const canvas = document.createElement('canvas');
    const size = 256;
    canvas.width = size; // 画布宽高
    canvas.height = size;
    const ctx = canvas.getContext('2d'); // 获取画布的2d绘图上下文
    ctx.clearRect(0, 0, size, size); // 清空整个画布区域
    ctx.fillStyle = color; // 绘制文本的颜色
    ctx.globalAlpha = opacity; // 绘制文本的透明度
    ctx.font = 'bold 64px Arial'; // 字体
    ctx.textAlign = 'center'; // 文本水平对其
    ctx.textBaseline = 'middle'; // 文本垂直对其
    ctx.fillText(text, size / 2, size / 2); // 画布中心绘制这个文字
    const texture = new THREE.CanvasTexture(canvas); // 用画布生成THREE.JS纹理,用于后续贴图使用
    texture.minFilter = THREE.LinearFilter; // 设置为线性过滤,避免缩放时产生模糊或者锯齿

    
    const material = new THREE.SpriteMaterial({ // 精灵材质
        map: texture, 
        transparent: true, 
        opacity: opacity, // 控制字体透明度,产生远近的效果
        depthWrite: false, // 关闭深度写入,能够减少透明物体的冲突
        depthTest: true,
    });
    // 用精灵物体更加快捷
    const sprite = new THREE.Sprite(material);
    sprite.scale.set(4, 4, 1);  // 缩放文字大小
    return sprite;
}
  • 借助canvas标签绘制文字,设置文字的大小 颜色 宽高 透明度 对齐效果
  • canvas标签借助CanvasTexture转化为一个贴图texture
  • SpriteMaterial精灵材质中,将texture赋值给map属性,同时注意开启透明transparent,设置透明度;关闭深度写入(这个很关键,不关闭的话,会出现抖动的情况)
  • 最后再创建精灵物体
  1. createTextGroup创建一个字体组合
js 复制代码
function createTextGroup(chars, opacity=1) {
    const group = new THREE.Group();
    for (let i = 0; i < chars.length; i++) {
        const sprite = createTextSprite(chars[i], '#5a3814', opacity);
        // 竖直排列的关键点
        sprite.position.set(0, -i * 1, 0);  // 竖直排列,间隔5单位
        group.add(sprite);
    }
    return group;
}
  • 这个方法的作用是,将教学相长这样的一个组合拆分,每个字单独调用createTextSprite方法,
  • 得到的sprite物体添加到group组合中
  • sprite.position.set(0, -i * 1, 0)这行代码是设置文字垂直排列的关键,教学相长的下面,所以他的y设置的是-i * 1,y轴越往上,值越大
  1. 遍历所有数据,创建文字组
js 复制代码
const allGroups = [];
groupsTexts.forEach((chars, idx) => {
    const group = createTextGroup(chars, Math.random());
    group.position.set(
        (Math.random() - 0.5) * 40,  // x随机散布
        Math.random() * 50 + 20,     // y开始在顶部附近
        (idx % 2) * 2               // z位置稍微有差异,制造远近
    ); // 每个字体组合的下降位置不尽相同
    allGroups.push({
        group,
        speed: Math.random() * Math.random() * 0.1 + 0.1 // 下降的速度
    });
    scene.add(group);
});

animate动画中:

js 复制代码
function animate() {
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed; // 下落的关键

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    renderer.render(scene, camera);
}

animate();

这样以后,能够实现如下效果:

4. 底部旋转同心圆

  1. 要创建的同心圆有5层,
js 复制代码
const circleList = [
    {
        radius: 6, 
        color: '#5a3814'
    },
    {
        radius: 12, 
        color: '#5a3814'
    },
    {
        radius: 18, 
        color: '#5a3814'
    },
    {
        radius: 24, 
        color: '#5a3814'
    },
    {
        radius: 30, 
        color: '#5a3814'
    }
]
let circleMeshs = []
circleList.forEach(item => {
    createCircle(item)
})
  1. createCircle方法,创建单个圆
js 复制代码
function createCircle (item) {
    // 创建渐变画布
    const size = 256;
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    
    const ctx = canvas.getContext('2d');
    // 设置渐变:createRadialGradient(x0, y0, r0, x1, y1, r1)  => (x0, y0)起始圆心坐标,r0是半径;
    // (x1, y1)结束圆圆心坐标,r1是结束圆半径,表示从中心到外围的镜像渐变
    const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size/2);
    gradient.addColorStop(0, 'white');
    gradient.addColorStop(1, item.color); // 从外到里渐变,最里面是白色
    // 设置画笔
    ctx.fillStyle = gradient;
    // 填充的范围是整个画布
    ctx.fillRect(0, 0, size, size);

    // 纹理
    const texture = new THREE.CanvasTexture(canvas);
    // 圆形平面
    const geometry = new THREE.CircleGeometry(item.radius, 32); // 半径1,32段细分
    const material = new THREE.MeshBasicMaterial({
        color: 0xe4d6c3, 
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        depthWrite: false, //深度写入
        depthTest: true, // 深度检测
        opacity: 0.1
    });
    const circle = new THREE.Mesh(geometry, material);
    circle.rotation.x = Math.PI / 2 // 沿着x轴旋转90°
    circle.position.y = -3
    scene.add(circle);
    // 将同心圆添加到数组中
    circleMeshs.push(circle)
}
  • 圆用的是CircleGeometry
  • 注意,必须关闭deepWrite深度写入,否则可能只能看到最外层的圆
  1. animate动画
diff 复制代码
function animate() {
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed;

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    // 让同心圆转起来
+    circleMeshs.forEach(circle => {
+        circle.rotation.z += 0.01 // 这里我有点困惑,实际上看起来是绕着y轴旋转的,但是设置必须设+置为z才行
+    })

    renderer.render(scene, camera);
}

animate();

这样以后的效果:

5. 文字标签

想要创建这样的文字标签,并且跟随同心圆一起移动:

  1. 创建标签的方法,注意标签有背景色,还有文字
js 复制代码
function createLabel (text) {
    // 用canvas写文字
    const width = 128
    const height = 128
    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height

    const ctx = canvas.getContext('2d')
    const gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2);
    gradient.addColorStop(0, '#ffffff'); // 边缘白色
    gradient.addColorStop(1, 'pink'); // 中心深棕色
    ctx.fillStyle = gradient // 画笔设置为渐变
    ctx.fillRect(0, 0, width, height) // 修改画布颜色

    ctx.font = '24px Arial'
    ctx.fillStyle = '#ae7c47' // 修改画笔的颜色来绘制文字
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.fillText(text, width / 2, height / 2) // 填充文字

    // canvas当做纹理贴图
    const texture = new THREE.CanvasTexture(canvas) 
    texture.minFilter = THREE.LinearFilter // 防止模糊

    // 平面几何体,稍宽,适合文字
    const geometry = new THREE.PlaneGeometry(3.5, 1.75); // 可以调整比例,使更扁宽贴圆

    const material = new THREE.MeshBasicMaterial({
        map: texture,
        transparent: true,
        side: THREE.DoubleSide,
        depthWrite: false,
        depthTest: true,
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh
}
  • 上面的步骤和之前的类似,唯一要注意的是canvas画布的背景和文字颜色不同,所以要记得修改画笔的颜色后再去绘制
js 复制代码
const labelGroup = new THREE.Group()
scene.add(labelGroup)
const radius4 = circleList[circleList.length - 2].radius
const radius5 = circleList[circleList.length - 1].radius 
// 让标签的位置处于第四和第五个圆之间
const labelRadius = (radius4 + radius5) / 2
// 标签内容
const labels = ['AG致用', '二元辩证', '原点规律', '历史背景', '成长案例'];
const labelCount = labels.length
for (let i = 0; i < labels.length; i++) {
    const angle = (i / labelCount) * Math.PI * 2
    const labelMesh = createLabel(labels[i])
    // 位置坐标转化
    labelMesh.position.set(
        labelRadius * Math.cos(angle),
        -3,
        labelRadius * Math.sin(angle),
    )
    labelMesh.lookAt(0, 0, 0)
    labelMesh.rotateY(Math.PI)
    labelGroup.add(labelMesh)
}

上面的坐标转化见下图:

这行代码是让标签均匀分布在圆上:

js 复制代码
const angle = (i / labelCount) * Math.PI * 2

最终在animate函数中更新标签的位置

diff 复制代码
function animate() {
    if (!isAnimate) return
    requestAnimationFrame(animate);

    // 文字组下落
    allGroups.forEach(item => {
        item.group.position.y -= item.speed;

        if (item.group.position.y < -3) {  // 下面越界后重置到顶部随机位置
            item.group.position.y = 50 + Math.random() * 20;
            item.group.position.x = (Math.random() - 0.5) * 40;
        }
    });

    // 让同心圆转起来
    circleMeshs.forEach(circle => {
        circle.rotation.z += 0.01
    })

    // 让标签转起来
+    labelGroup.rotation.y += 0.01

    renderer.render(scene, camera);
}
相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆1 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师2 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆2 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端